diff options
author | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
---|---|---|
committer | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
commit | 4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch) | |
tree | ee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/xlsxwriter | |
parent | cc961e04ba734dd72309fb548a2f97d67d578813 (diff) | |
download | gn-ai-master.tar.gz |
Diffstat (limited to '.venv/lib/python3.12/site-packages/xlsxwriter')
38 files changed, 25580 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/__init__.py b/.venv/lib/python3.12/site-packages/xlsxwriter/__init__.py new file mode 100644 index 00000000..db42495c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/__init__.py @@ -0,0 +1,8 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# +__version__ = "3.2.2" +__VERSION__ = __version__ +from .workbook import Workbook # noqa diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/app.py b/.venv/lib/python3.12/site-packages/xlsxwriter/app.py new file mode 100644 index 00000000..bf565b54 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/app.py @@ -0,0 +1,199 @@ +############################################################################### +# +# App - A class for writing the Excel XLSX App file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Package imports. +from . import xmlwriter + + +class App(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX App file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + self.part_names = [] + self.heading_pairs = [] + self.properties = {} + self.doc_security = 0 + + def _add_part_name(self, part_name): + # Add the name of a workbook Part such as 'Sheet1' or 'Print_Titles'. + self.part_names.append(part_name) + + def _add_heading_pair(self, heading_pair): + # Add the name of a workbook Heading Pair such as 'Worksheets', + # 'Charts' or 'Named Ranges'. + + # Ignore empty pairs such as chartsheets. + if not heading_pair[1]: + return + + self.heading_pairs.append(("lpstr", heading_pair[0])) + self.heading_pairs.append(("i4", heading_pair[1])) + + def _set_properties(self, properties): + # Set the document properties. + self.properties = properties + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + self._write_properties() + self._write_application() + self._write_doc_security() + self._write_scale_crop() + self._write_heading_pairs() + self._write_titles_of_parts() + self._write_manager() + self._write_company() + self._write_links_up_to_date() + self._write_shared_doc() + self._write_hyperlink_base() + self._write_hyperlinks_changed() + self._write_app_version() + + self._xml_end_tag("Properties") + + # Close the file. + self._xml_close() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_properties(self): + # Write the <Properties> element. + schema = "http://schemas.openxmlformats.org/officeDocument/2006/" + xmlns = schema + "extended-properties" + xmlns_vt = schema + "docPropsVTypes" + + attributes = [ + ("xmlns", xmlns), + ("xmlns:vt", xmlns_vt), + ] + + self._xml_start_tag("Properties", attributes) + + def _write_application(self): + # Write the <Application> element. + self._xml_data_element("Application", "Microsoft Excel") + + def _write_doc_security(self): + # Write the <DocSecurity> element. + self._xml_data_element("DocSecurity", self.doc_security) + + def _write_scale_crop(self): + # Write the <ScaleCrop> element. + self._xml_data_element("ScaleCrop", "false") + + def _write_heading_pairs(self): + # Write the <HeadingPairs> element. + self._xml_start_tag("HeadingPairs") + self._write_vt_vector("variant", self.heading_pairs) + self._xml_end_tag("HeadingPairs") + + def _write_titles_of_parts(self): + # Write the <TitlesOfParts> element. + parts_data = [] + + self._xml_start_tag("TitlesOfParts") + + for part_name in self.part_names: + parts_data.append(("lpstr", part_name)) + + self._write_vt_vector("lpstr", parts_data) + + self._xml_end_tag("TitlesOfParts") + + def _write_vt_vector(self, base_type, vector_data): + # Write the <vt:vector> element. + attributes = [ + ("size", len(vector_data)), + ("baseType", base_type), + ] + + self._xml_start_tag("vt:vector", attributes) + + for vt_data in vector_data: + if base_type == "variant": + self._xml_start_tag("vt:variant") + + self._write_vt_data(vt_data) + + if base_type == "variant": + self._xml_end_tag("vt:variant") + + self._xml_end_tag("vt:vector") + + def _write_vt_data(self, vt_data): + # Write the <vt:*> elements such as <vt:lpstr> and <vt:if>. + self._xml_data_element(f"vt:{vt_data[0]}", vt_data[1]) + + def _write_company(self): + company = self.properties.get("company", "") + + self._xml_data_element("Company", company) + + def _write_manager(self): + # Write the <Manager> element. + if "manager" not in self.properties: + return + + self._xml_data_element("Manager", self.properties["manager"]) + + def _write_links_up_to_date(self): + # Write the <LinksUpToDate> element. + self._xml_data_element("LinksUpToDate", "false") + + def _write_shared_doc(self): + # Write the <SharedDoc> element. + self._xml_data_element("SharedDoc", "false") + + def _write_hyperlink_base(self): + # Write the <HyperlinkBase> element. + hyperlink_base = self.properties.get("hyperlink_base") + + if hyperlink_base is None: + return + + self._xml_data_element("HyperlinkBase", hyperlink_base) + + def _write_hyperlinks_changed(self): + # Write the <HyperlinksChanged> element. + self._xml_data_element("HyperlinksChanged", "false") + + def _write_app_version(self): + # Write the <AppVersion> element. + self._xml_data_element("AppVersion", "12.0000") diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/chart.py b/.venv/lib/python3.12/site-packages/xlsxwriter/chart.py new file mode 100644 index 00000000..08151aed --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/chart.py @@ -0,0 +1,4382 @@ +############################################################################### +# +# Chart - A class for writing the Excel XLSX Worksheet file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +import copy +import re +from warnings import warn + +from . import xmlwriter +from .shape import Shape +from .utility import ( + _datetime_to_excel_datetime, + _get_rgb_color, + _supported_datetime, + quote_sheetname, + xl_range_formula, + xl_rowcol_to_cell, +) + + +class Chart(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX Chart file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + self.subtype = None + self.sheet_type = 0x0200 + self.orientation = 0x0 + self.series = [] + self.embedded = 0 + self.id = -1 + self.series_index = 0 + self.style_id = 2 + self.axis_ids = [] + self.axis2_ids = [] + self.cat_has_num_fmt = 0 + self.requires_category = False + self.legend = {} + self.cat_axis_position = "b" + self.val_axis_position = "l" + self.formula_ids = {} + self.formula_data = [] + self.horiz_cat_axis = 0 + self.horiz_val_axis = 1 + self.protection = 0 + self.chartarea = {} + self.plotarea = {} + self.x_axis = {} + self.y_axis = {} + self.y2_axis = {} + self.x2_axis = {} + self.chart_name = "" + self.show_blanks = "gap" + self.show_na_as_empty = False + self.show_hidden = False + self.show_crosses = True + self.width = 480 + self.height = 288 + self.x_scale = 1 + self.y_scale = 1 + self.x_offset = 0 + self.y_offset = 0 + self.table = None + self.cross_between = "between" + self.default_marker = None + self.series_gap_1 = None + self.series_gap_2 = None + self.series_overlap_1 = None + self.series_overlap_2 = None + self.drop_lines = None + self.hi_low_lines = None + self.up_down_bars = None + self.smooth_allowed = False + self.title_font = None + self.title_name = None + self.title_formula = None + self.title_data_id = None + self.title_layout = None + self.title_overlay = None + self.title_none = False + self.date_category = False + self.date_1904 = False + self.remove_timezone = False + self.label_positions = {} + self.label_position_default = "" + self.already_inserted = False + self.combined = None + self.is_secondary = False + self.warn_sheetname = True + self._set_default_properties() + self.fill = {} + + def add_series(self, options=None): + """ + Add a data series to a chart. + + Args: + options: A dictionary of chart series options. + + Returns: + Nothing. + + """ + # Add a series and it's properties to a chart. + if options is None: + options = {} + + # Check that the required input has been specified. + if "values" not in options: + warn("Must specify 'values' in add_series()") + return + + if self.requires_category and "categories" not in options: + warn("Must specify 'categories' in add_series() for this chart type") + return + + if len(self.series) == 255: + warn( + "The maximum number of series that can be added to an " + "Excel Chart is 255" + ) + return + + # Convert list into a formula string. + values = self._list_to_formula(options.get("values")) + categories = self._list_to_formula(options.get("categories")) + + # Switch name and name_formula parameters if required. + name, name_formula = self._process_names( + options.get("name"), options.get("name_formula") + ) + + # Get an id for the data equivalent to the range formula. + cat_id = self._get_data_id(categories, options.get("categories_data")) + val_id = self._get_data_id(values, options.get("values_data")) + name_id = self._get_data_id(name_formula, options.get("name_data")) + + # Set the line properties for the series. + line = Shape._get_line_properties(options.get("line")) + + # Allow 'border' as a synonym for 'line' in bar/column style charts. + if options.get("border"): + line = Shape._get_line_properties(options["border"]) + + # Set the fill properties for the series. + fill = Shape._get_fill_properties(options.get("fill")) + + # Set the pattern fill properties for the series. + pattern = Shape._get_pattern_properties(options.get("pattern")) + + # Set the gradient fill properties for the series. + gradient = Shape._get_gradient_properties(options.get("gradient")) + + # Pattern fill overrides solid fill. + if pattern: + self.fill = None + + # Gradient fill overrides the solid and pattern fill. + if gradient: + pattern = None + fill = None + + # Set the marker properties for the series. + marker = self._get_marker_properties(options.get("marker")) + + # Set the trendline properties for the series. + trendline = self._get_trendline_properties(options.get("trendline")) + + # Set the line smooth property for the series. + smooth = options.get("smooth") + + # Set the error bars properties for the series. + y_error_bars = self._get_error_bars_props(options.get("y_error_bars")) + x_error_bars = self._get_error_bars_props(options.get("x_error_bars")) + + error_bars = {"x_error_bars": x_error_bars, "y_error_bars": y_error_bars} + + # Set the point properties for the series. + points = self._get_points_properties(options.get("points")) + + # Set the labels properties for the series. + labels = self._get_labels_properties(options.get("data_labels")) + + # Set the "invert if negative" fill property. + invert_if_neg = options.get("invert_if_negative", False) + inverted_color = options.get("invert_if_negative_color", False) + + # Set the secondary axis properties. + x2_axis = options.get("x2_axis") + y2_axis = options.get("y2_axis") + + # Store secondary status for combined charts. + if x2_axis or y2_axis: + self.is_secondary = True + + # Set the gap for Bar/Column charts. + if options.get("gap") is not None: + if y2_axis: + self.series_gap_2 = options["gap"] + else: + self.series_gap_1 = options["gap"] + + # Set the overlap for Bar/Column charts. + if options.get("overlap"): + if y2_axis: + self.series_overlap_2 = options["overlap"] + else: + self.series_overlap_1 = options["overlap"] + + # Add the user supplied data to the internal structures. + series = { + "values": values, + "categories": categories, + "name": name, + "name_formula": name_formula, + "name_id": name_id, + "val_data_id": val_id, + "cat_data_id": cat_id, + "line": line, + "fill": fill, + "pattern": pattern, + "gradient": gradient, + "marker": marker, + "trendline": trendline, + "labels": labels, + "invert_if_neg": invert_if_neg, + "inverted_color": inverted_color, + "x2_axis": x2_axis, + "y2_axis": y2_axis, + "points": points, + "error_bars": error_bars, + "smooth": smooth, + } + + self.series.append(series) + + def set_x_axis(self, options): + """ + Set the chart X axis options. + + Args: + options: A dictionary of axis options. + + Returns: + Nothing. + + """ + axis = self._convert_axis_args(self.x_axis, options) + + self.x_axis = axis + + def set_y_axis(self, options): + """ + Set the chart Y axis options. + + Args: + options: A dictionary of axis options. + + Returns: + Nothing. + + """ + axis = self._convert_axis_args(self.y_axis, options) + + self.y_axis = axis + + def set_x2_axis(self, options): + """ + Set the chart secondary X axis options. + + Args: + options: A dictionary of axis options. + + Returns: + Nothing. + + """ + axis = self._convert_axis_args(self.x2_axis, options) + + self.x2_axis = axis + + def set_y2_axis(self, options): + """ + Set the chart secondary Y axis options. + + Args: + options: A dictionary of axis options. + + Returns: + Nothing. + + """ + axis = self._convert_axis_args(self.y2_axis, options) + + self.y2_axis = axis + + def set_title(self, options=None): + """ + Set the chart title options. + + Args: + options: A dictionary of chart title options. + + Returns: + Nothing. + + """ + if options is None: + options = {} + + name, name_formula = self._process_names( + options.get("name"), options.get("name_formula") + ) + + data_id = self._get_data_id(name_formula, options.get("data")) + + self.title_name = name + self.title_formula = name_formula + self.title_data_id = data_id + + # Set the font properties if present. + self.title_font = self._convert_font_args(options.get("name_font")) + + # Set the axis name layout. + self.title_layout = self._get_layout_properties(options.get("layout"), True) + # Set the title overlay option. + self.title_overlay = options.get("overlay") + + # Set the automatic title option. + self.title_none = options.get("none") + + def set_legend(self, options): + """ + Set the chart legend options. + + Args: + options: A dictionary of chart legend options. + + Returns: + Nothing. + """ + # Convert the user defined properties to internal properties. + self.legend = self._get_legend_properties(options) + + def set_plotarea(self, options): + """ + Set the chart plot area options. + + Args: + options: A dictionary of chart plot area options. + + Returns: + Nothing. + """ + # Convert the user defined properties to internal properties. + self.plotarea = self._get_area_properties(options) + + def set_chartarea(self, options): + """ + Set the chart area options. + + Args: + options: A dictionary of chart area options. + + Returns: + Nothing. + """ + # Convert the user defined properties to internal properties. + self.chartarea = self._get_area_properties(options) + + def set_style(self, style_id): + """ + Set the chart style type. + + Args: + style_id: An int representing the chart style. + + Returns: + Nothing. + """ + # Set one of the 48 built-in Excel chart styles. The default is 2. + if style_id is None: + style_id = 2 + + if style_id < 1 or style_id > 48: + style_id = 2 + + self.style_id = style_id + + def show_blanks_as(self, option): + """ + Set the option for displaying blank data in a chart. + + Args: + option: A string representing the display option. + + Returns: + Nothing. + """ + if not option: + return + + valid_options = { + "gap": 1, + "zero": 1, + "span": 1, + } + + if option not in valid_options: + warn(f"Unknown show_blanks_as() option '{option}'") + return + + self.show_blanks = option + + def show_na_as_empty_cell(self): + """ + Display ``#N/A`` on charts as blank/empty cells. + + Args: + None. + + Returns: + Nothing. + """ + self.show_na_as_empty = True + + def show_hidden_data(self): + """ + Display data on charts from hidden rows or columns. + + Args: + None. + + Returns: + Nothing. + """ + self.show_hidden = True + + def set_size(self, options=None): + """ + Set size or scale of the chart. + + Args: + options: A dictionary of chart size options. + + Returns: + Nothing. + """ + if options is None: + options = {} + + # Set dimensions or scale for the chart. + self.width = options.get("width", self.width) + self.height = options.get("height", self.height) + self.x_scale = options.get("x_scale", 1) + self.y_scale = options.get("y_scale", 1) + self.x_offset = options.get("x_offset", 0) + self.y_offset = options.get("y_offset", 0) + + def set_table(self, options=None): + """ + Set properties for an axis data table. + + Args: + options: A dictionary of axis table options. + + Returns: + Nothing. + + """ + if options is None: + options = {} + + table = {} + + table["horizontal"] = options.get("horizontal", 1) + table["vertical"] = options.get("vertical", 1) + table["outline"] = options.get("outline", 1) + table["show_keys"] = options.get("show_keys", 0) + table["font"] = self._convert_font_args(options.get("font")) + + self.table = table + + def set_up_down_bars(self, options=None): + """ + Set properties for the chart up-down bars. + + Args: + options: A dictionary of options. + + Returns: + Nothing. + + """ + if options is None: + options = {} + + # Defaults. + up_line = None + up_fill = None + down_line = None + down_fill = None + + # Set properties for 'up' bar. + if options.get("up"): + if "border" in options["up"]: + # Map border to line. + up_line = Shape._get_line_properties(options["up"]["border"]) + + if "line" in options["up"]: + up_line = Shape._get_line_properties(options["up"]["line"]) + + if "fill" in options["up"]: + up_fill = Shape._get_fill_properties(options["up"]["fill"]) + + # Set properties for 'down' bar. + if options.get("down"): + if "border" in options["down"]: + # Map border to line. + down_line = Shape._get_line_properties(options["down"]["border"]) + + if "line" in options["down"]: + down_line = Shape._get_line_properties(options["down"]["line"]) + + if "fill" in options["down"]: + down_fill = Shape._get_fill_properties(options["down"]["fill"]) + + self.up_down_bars = { + "up": { + "line": up_line, + "fill": up_fill, + }, + "down": { + "line": down_line, + "fill": down_fill, + }, + } + + def set_drop_lines(self, options=None): + """ + Set properties for the chart drop lines. + + Args: + options: A dictionary of options. + + Returns: + Nothing. + + """ + if options is None: + options = {} + + line = Shape._get_line_properties(options.get("line")) + fill = Shape._get_fill_properties(options.get("fill")) + + # Set the pattern fill properties for the series. + pattern = Shape._get_pattern_properties(options.get("pattern")) + + # Set the gradient fill properties for the series. + gradient = Shape._get_gradient_properties(options.get("gradient")) + + # Pattern fill overrides solid fill. + if pattern: + self.fill = None + + # Gradient fill overrides the solid and pattern fill. + if gradient: + pattern = None + fill = None + + self.drop_lines = { + "line": line, + "fill": fill, + "pattern": pattern, + "gradient": gradient, + } + + def set_high_low_lines(self, options=None): + """ + Set properties for the chart high-low lines. + + Args: + options: A dictionary of options. + + Returns: + Nothing. + + """ + if options is None: + options = {} + + line = Shape._get_line_properties(options.get("line")) + fill = Shape._get_fill_properties(options.get("fill")) + + # Set the pattern fill properties for the series. + pattern = Shape._get_pattern_properties(options.get("pattern")) + + # Set the gradient fill properties for the series. + gradient = Shape._get_gradient_properties(options.get("gradient")) + + # Pattern fill overrides solid fill. + if pattern: + self.fill = None + + # Gradient fill overrides the solid and pattern fill. + if gradient: + pattern = None + fill = None + + self.hi_low_lines = { + "line": line, + "fill": fill, + "pattern": pattern, + "gradient": gradient, + } + + def combine(self, chart=None): + """ + Create a combination chart with a secondary chart. + + Args: + chart: The secondary chart to combine with the primary chart. + + Returns: + Nothing. + + """ + if chart is None: + return + + self.combined = chart + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + # Write the c:chartSpace element. + self._write_chart_space() + + # Write the c:lang element. + self._write_lang() + + # Write the c:style element. + self._write_style() + + # Write the c:protection element. + self._write_protection() + + # Write the c:chart element. + self._write_chart() + + # Write the c:spPr element for the chartarea formatting. + self._write_sp_pr(self.chartarea) + + # Write the c:printSettings element. + if self.embedded: + self._write_print_settings() + + # Close the worksheet tag. + self._xml_end_tag("c:chartSpace") + # Close the file. + self._xml_close() + + def _convert_axis_args(self, axis, user_options): + # Convert user defined axis values into private hash values. + options = axis["defaults"].copy() + options.update(user_options) + + name, name_formula = self._process_names( + options.get("name"), options.get("name_formula") + ) + + data_id = self._get_data_id(name_formula, options.get("data")) + + axis = { + "defaults": axis["defaults"], + "name": name, + "formula": name_formula, + "data_id": data_id, + "reverse": options.get("reverse"), + "min": options.get("min"), + "max": options.get("max"), + "minor_unit": options.get("minor_unit"), + "major_unit": options.get("major_unit"), + "minor_unit_type": options.get("minor_unit_type"), + "major_unit_type": options.get("major_unit_type"), + "display_units": options.get("display_units"), + "log_base": options.get("log_base"), + "crossing": options.get("crossing"), + "position_axis": options.get("position_axis"), + "position": options.get("position"), + "label_position": options.get("label_position"), + "label_align": options.get("label_align"), + "num_format": options.get("num_format"), + "num_format_linked": options.get("num_format_linked"), + "interval_unit": options.get("interval_unit"), + "interval_tick": options.get("interval_tick"), + "text_axis": False, + } + + axis["visible"] = options.get("visible", True) + + # Convert the display units. + axis["display_units"] = self._get_display_units(axis["display_units"]) + axis["display_units_visible"] = options.get("display_units_visible", True) + + # Map major_gridlines properties. + if options.get("major_gridlines") and options["major_gridlines"]["visible"]: + axis["major_gridlines"] = self._get_gridline_properties( + options["major_gridlines"] + ) + + # Map minor_gridlines properties. + if options.get("minor_gridlines") and options["minor_gridlines"]["visible"]: + axis["minor_gridlines"] = self._get_gridline_properties( + options["minor_gridlines"] + ) + + # Only use the first letter of bottom, top, left or right. + if axis.get("position"): + axis["position"] = axis["position"].lower()[0] + + # Set the position for a category axis on or between the tick marks. + if axis.get("position_axis"): + if axis["position_axis"] == "on_tick": + axis["position_axis"] = "midCat" + elif axis["position_axis"] == "between": + # Doesn't need to be modified. + pass + else: + # Otherwise use the default value. + axis["position_axis"] = None + + # Set the category axis as a date axis. + if options.get("date_axis"): + self.date_category = True + + # Set the category axis as a text axis. + if options.get("text_axis"): + self.date_category = False + axis["text_axis"] = True + + # Convert datetime args if required. + if axis.get("min") and _supported_datetime(axis["min"]): + axis["min"] = _datetime_to_excel_datetime( + axis["min"], self.date_1904, self.remove_timezone + ) + if axis.get("max") and _supported_datetime(axis["max"]): + axis["max"] = _datetime_to_excel_datetime( + axis["max"], self.date_1904, self.remove_timezone + ) + if axis.get("crossing") and _supported_datetime(axis["crossing"]): + axis["crossing"] = _datetime_to_excel_datetime( + axis["crossing"], self.date_1904, self.remove_timezone + ) + + # Set the font properties if present. + axis["num_font"] = self._convert_font_args(options.get("num_font")) + axis["name_font"] = self._convert_font_args(options.get("name_font")) + + # Set the axis name layout. + axis["name_layout"] = self._get_layout_properties( + options.get("name_layout"), True + ) + + # Set the line properties for the axis. + axis["line"] = Shape._get_line_properties(options.get("line")) + + # Set the fill properties for the axis. + axis["fill"] = Shape._get_fill_properties(options.get("fill")) + + # Set the pattern fill properties for the series. + axis["pattern"] = Shape._get_pattern_properties(options.get("pattern")) + + # Set the gradient fill properties for the series. + axis["gradient"] = Shape._get_gradient_properties(options.get("gradient")) + + # Pattern fill overrides solid fill. + if axis.get("pattern"): + axis["fill"] = None + + # Gradient fill overrides the solid and pattern fill. + if axis.get("gradient"): + axis["pattern"] = None + axis["fill"] = None + + # Set the tick marker types. + axis["minor_tick_mark"] = self._get_tick_type(options.get("minor_tick_mark")) + axis["major_tick_mark"] = self._get_tick_type(options.get("major_tick_mark")) + + return axis + + def _convert_font_args(self, options): + # Convert user defined font values into private dict values. + if not options: + return {} + + font = { + "name": options.get("name"), + "color": options.get("color"), + "size": options.get("size"), + "bold": options.get("bold"), + "italic": options.get("italic"), + "underline": options.get("underline"), + "pitch_family": options.get("pitch_family"), + "charset": options.get("charset"), + "baseline": options.get("baseline", 0), + "rotation": options.get("rotation"), + } + + # Convert font size units. + if font["size"]: + font["size"] = int(font["size"] * 100) + + # Convert rotation into 60,000ths of a degree. + if font["rotation"]: + font["rotation"] = 60000 * int(font["rotation"]) + + return font + + def _list_to_formula(self, data): + # Convert and list of row col values to a range formula. + + # If it isn't an array ref it is probably a formula already. + if not isinstance(data, list): + # Check for unquoted sheetnames. + if data and " " in data and "'" not in data and self.warn_sheetname: + warn( + f"Sheetname in '{data}' contains spaces but isn't quoted. " + f"This may cause an error in Excel." + ) + return data + + formula = xl_range_formula(*data) + + return formula + + def _process_names(self, name, name_formula): + # Switch name and name_formula parameters if required. + + if name is not None: + if isinstance(name, list): + # Convert a list of values into a name formula. + cell = xl_rowcol_to_cell(name[1], name[2], True, True) + name_formula = quote_sheetname(name[0]) + "!" + cell + name = "" + elif re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", name): + # Name looks like a formula, use it to set name_formula. + name_formula = name + name = "" + + return name, name_formula + + def _get_data_type(self, data): + # Find the overall type of the data associated with a series. + + # Check for no data in the series. + if data is None or len(data) == 0: + return "none" + + if isinstance(data[0], list): + return "multi_str" + + # Determine if data is numeric or strings. + for token in data: + if token is None: + continue + + # Check for strings that would evaluate to float like + # '1.1_1' of ' 1'. + if isinstance(token, str) and re.search("[_ ]", token): + # Assume entire data series is string data. + return "str" + + try: + float(token) + except ValueError: + # Not a number. Assume entire data series is string data. + return "str" + + # The series data was all numeric. + return "num" + + def _get_data_id(self, formula, data): + # Assign an id to a each unique series formula or title/axis formula. + # Repeated formulas such as for categories get the same id. If the + # series or title has user specified data associated with it then + # that is also stored. This data is used to populate cached Excel + # data when creating a chart. If there is no user defined data then + # it will be populated by the parent Workbook._add_chart_data(). + + # Ignore series without a range formula. + if not formula: + return None + + # Strip the leading '=' from the formula. + if formula.startswith("="): + formula = formula.lstrip("=") + + # Store the data id in a hash keyed by the formula and store the data + # in a separate array with the same id. + if formula not in self.formula_ids: + # Haven't seen this formula before. + formula_id = len(self.formula_data) + + self.formula_data.append(data) + self.formula_ids[formula] = formula_id + else: + # Formula already seen. Return existing id. + formula_id = self.formula_ids[formula] + + # Store user defined data if it isn't already there. + if self.formula_data[formula_id] is None: + self.formula_data[formula_id] = data + + return formula_id + + def _get_marker_properties(self, marker): + # Convert user marker properties to the structure required internally. + + if not marker: + return None + + # Copy the user defined properties since they will be modified. + marker = copy.deepcopy(marker) + + types = { + "automatic": "automatic", + "none": "none", + "square": "square", + "diamond": "diamond", + "triangle": "triangle", + "x": "x", + "star": "star", + "dot": "dot", + "short_dash": "dot", + "dash": "dash", + "long_dash": "dash", + "circle": "circle", + "plus": "plus", + "picture": "picture", + } + + # Check for valid types. + marker_type = marker.get("type") + + if marker_type is not None: + if marker_type in types: + marker["type"] = types[marker_type] + else: + warn(f"Unknown marker type '{marker_type}") + return None + + # Set the line properties for the marker. + line = Shape._get_line_properties(marker.get("line")) + + # Allow 'border' as a synonym for 'line'. + if "border" in marker: + line = Shape._get_line_properties(marker["border"]) + + # Set the fill properties for the marker. + fill = Shape._get_fill_properties(marker.get("fill")) + + # Set the pattern fill properties for the series. + pattern = Shape._get_pattern_properties(marker.get("pattern")) + + # Set the gradient fill properties for the series. + gradient = Shape._get_gradient_properties(marker.get("gradient")) + + # Pattern fill overrides solid fill. + if pattern: + self.fill = None + + # Gradient fill overrides the solid and pattern fill. + if gradient: + pattern = None + fill = None + + marker["line"] = line + marker["fill"] = fill + marker["pattern"] = pattern + marker["gradient"] = gradient + + return marker + + def _get_trendline_properties(self, trendline): + # Convert user trendline properties to structure required internally. + + if not trendline: + return None + + # Copy the user defined properties since they will be modified. + trendline = copy.deepcopy(trendline) + + types = { + "exponential": "exp", + "linear": "linear", + "log": "log", + "moving_average": "movingAvg", + "polynomial": "poly", + "power": "power", + } + + # Check the trendline type. + trend_type = trendline.get("type") + + if trend_type in types: + trendline["type"] = types[trend_type] + else: + warn(f"Unknown trendline type '{trend_type}'") + return None + + # Set the line properties for the trendline. + line = Shape._get_line_properties(trendline.get("line")) + + # Allow 'border' as a synonym for 'line'. + if "border" in trendline: + line = Shape._get_line_properties(trendline["border"]) + + # Set the fill properties for the trendline. + fill = Shape._get_fill_properties(trendline.get("fill")) + + # Set the pattern fill properties for the trendline. + pattern = Shape._get_pattern_properties(trendline.get("pattern")) + + # Set the gradient fill properties for the trendline. + gradient = Shape._get_gradient_properties(trendline.get("gradient")) + + # Set the format properties for the trendline label. + label = self._get_trendline_label_properties(trendline.get("label")) + + # Pattern fill overrides solid fill. + if pattern: + self.fill = None + + # Gradient fill overrides the solid and pattern fill. + if gradient: + pattern = None + fill = None + + trendline["line"] = line + trendline["fill"] = fill + trendline["pattern"] = pattern + trendline["gradient"] = gradient + trendline["label"] = label + + return trendline + + def _get_trendline_label_properties(self, label): + # Convert user trendline properties to structure required internally. + + if not label: + return {} + + # Copy the user defined properties since they will be modified. + label = copy.deepcopy(label) + + # Set the font properties if present. + font = self._convert_font_args(label.get("font")) + + # Set the line properties for the label. + line = Shape._get_line_properties(label.get("line")) + + # Allow 'border' as a synonym for 'line'. + if "border" in label: + line = Shape._get_line_properties(label["border"]) + + # Set the fill properties for the label. + fill = Shape._get_fill_properties(label.get("fill")) + + # Set the pattern fill properties for the label. + pattern = Shape._get_pattern_properties(label.get("pattern")) + + # Set the gradient fill properties for the label. + gradient = Shape._get_gradient_properties(label.get("gradient")) + + # Pattern fill overrides solid fill. + if pattern: + self.fill = None + + # Gradient fill overrides the solid and pattern fill. + if gradient: + pattern = None + fill = None + + label["font"] = font + label["line"] = line + label["fill"] = fill + label["pattern"] = pattern + label["gradient"] = gradient + + return label + + def _get_error_bars_props(self, options): + # Convert user error bars properties to structure required internally. + if not options: + return {} + + # Default values. + error_bars = {"type": "fixedVal", "value": 1, "endcap": 1, "direction": "both"} + + types = { + "fixed": "fixedVal", + "percentage": "percentage", + "standard_deviation": "stdDev", + "standard_error": "stdErr", + "custom": "cust", + } + + # Check the error bars type. + error_type = options["type"] + + if error_type in types: + error_bars["type"] = types[error_type] + else: + warn(f"Unknown error bars type '{error_type}") + return {} + + # Set the value for error types that require it. + if "value" in options: + error_bars["value"] = options["value"] + + # Set the end-cap style. + if "end_style" in options: + error_bars["endcap"] = options["end_style"] + + # Set the error bar direction. + if "direction" in options: + if options["direction"] == "minus": + error_bars["direction"] = "minus" + elif options["direction"] == "plus": + error_bars["direction"] = "plus" + else: + # Default to 'both'. + pass + + # Set any custom values. + error_bars["plus_values"] = options.get("plus_values") + error_bars["minus_values"] = options.get("minus_values") + error_bars["plus_data"] = options.get("plus_data") + error_bars["minus_data"] = options.get("minus_data") + + # Set the line properties for the error bars. + error_bars["line"] = Shape._get_line_properties(options.get("line")) + + return error_bars + + def _get_gridline_properties(self, options): + # Convert user gridline properties to structure required internally. + + # Set the visible property for the gridline. + gridline = {"visible": options.get("visible")} + + # Set the line properties for the gridline. + gridline["line"] = Shape._get_line_properties(options.get("line")) + + return gridline + + def _get_labels_properties(self, labels): + # Convert user labels properties to the structure required internally. + + if not labels: + return None + + # Copy the user defined properties since they will be modified. + labels = copy.deepcopy(labels) + + # Map user defined label positions to Excel positions. + position = labels.get("position") + + if position: + if position in self.label_positions: + if position == self.label_position_default: + labels["position"] = None + else: + labels["position"] = self.label_positions[position] + else: + warn(f"Unsupported label position '{position}' for this chart type") + return None + + # Map the user defined label separator to the Excel separator. + separator = labels.get("separator") + separators = { + ",": ", ", + ";": "; ", + ".": ". ", + "\n": "\n", + " ": " ", + } + + if separator: + if separator in separators: + labels["separator"] = separators[separator] + else: + warn("Unsupported label separator") + return None + + # Set the font properties if present. + labels["font"] = self._convert_font_args(labels.get("font")) + + # Set the line properties for the labels. + line = Shape._get_line_properties(labels.get("line")) + + # Allow 'border' as a synonym for 'line'. + if "border" in labels: + line = Shape._get_line_properties(labels["border"]) + + # Set the fill properties for the labels. + fill = Shape._get_fill_properties(labels.get("fill")) + + # Set the pattern fill properties for the labels. + pattern = Shape._get_pattern_properties(labels.get("pattern")) + + # Set the gradient fill properties for the labels. + gradient = Shape._get_gradient_properties(labels.get("gradient")) + + # Pattern fill overrides solid fill. + if pattern: + self.fill = None + + # Gradient fill overrides the solid and pattern fill. + if gradient: + pattern = None + fill = None + + labels["line"] = line + labels["fill"] = fill + labels["pattern"] = pattern + labels["gradient"] = gradient + + if labels.get("custom"): + for label in labels["custom"]: + if label is None: + continue + + value = label.get("value") + if value and re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", str(value)): + label["formula"] = value + + formula = label.get("formula") + if formula and formula.startswith("="): + label["formula"] = formula.lstrip("=") + + data_id = self._get_data_id(formula, label.get("data")) + label["data_id"] = data_id + + label["font"] = self._convert_font_args(label.get("font")) + + # Set the line properties for the label. + line = Shape._get_line_properties(label.get("line")) + + # Allow 'border' as a synonym for 'line'. + if "border" in label: + line = Shape._get_line_properties(label["border"]) + + # Set the fill properties for the label. + fill = Shape._get_fill_properties(label.get("fill")) + + # Set the pattern fill properties for the label. + pattern = Shape._get_pattern_properties(label.get("pattern")) + + # Set the gradient fill properties for the label. + gradient = Shape._get_gradient_properties(label.get("gradient")) + + # Pattern fill overrides solid fill. + if pattern: + self.fill = None + + # Gradient fill overrides the solid and pattern fill. + if gradient: + pattern = None + fill = None + + label["line"] = line + label["fill"] = fill + label["pattern"] = pattern + label["gradient"] = gradient + + return labels + + def _get_area_properties(self, options): + # Convert user area properties to the structure required internally. + area = {} + + # Set the line properties for the chartarea. + line = Shape._get_line_properties(options.get("line")) + + # Allow 'border' as a synonym for 'line'. + if options.get("border"): + line = Shape._get_line_properties(options["border"]) + + # Set the fill properties for the chartarea. + fill = Shape._get_fill_properties(options.get("fill")) + + # Set the pattern fill properties for the series. + pattern = Shape._get_pattern_properties(options.get("pattern")) + + # Set the gradient fill properties for the series. + gradient = Shape._get_gradient_properties(options.get("gradient")) + + # Pattern fill overrides solid fill. + if pattern: + self.fill = None + + # Gradient fill overrides the solid and pattern fill. + if gradient: + pattern = None + fill = None + + # Set the plotarea layout. + layout = self._get_layout_properties(options.get("layout"), False) + + area["line"] = line + area["fill"] = fill + area["pattern"] = pattern + area["layout"] = layout + area["gradient"] = gradient + + return area + + def _get_legend_properties(self, options=None): + # Convert user legend properties to the structure required internally. + legend = {} + + if options is None: + options = {} + + legend["position"] = options.get("position", "right") + legend["delete_series"] = options.get("delete_series") + legend["font"] = self._convert_font_args(options.get("font")) + legend["layout"] = self._get_layout_properties(options.get("layout"), False) + + # Turn off the legend. + if options.get("none"): + legend["position"] = "none" + + # Set the line properties for the legend. + line = Shape._get_line_properties(options.get("line")) + + # Allow 'border' as a synonym for 'line'. + if options.get("border"): + line = Shape._get_line_properties(options["border"]) + + # Set the fill properties for the legend. + fill = Shape._get_fill_properties(options.get("fill")) + + # Set the pattern fill properties for the series. + pattern = Shape._get_pattern_properties(options.get("pattern")) + + # Set the gradient fill properties for the series. + gradient = Shape._get_gradient_properties(options.get("gradient")) + + # Pattern fill overrides solid fill. + if pattern: + self.fill = None + + # Gradient fill overrides the solid and pattern fill. + if gradient: + pattern = None + fill = None + + # Set the legend layout. + layout = self._get_layout_properties(options.get("layout"), False) + + legend["line"] = line + legend["fill"] = fill + legend["pattern"] = pattern + legend["layout"] = layout + legend["gradient"] = gradient + + return legend + + def _get_layout_properties(self, args, is_text): + # Convert user defined layout properties to format used internally. + layout = {} + + if not args: + return {} + + if is_text: + properties = ("x", "y") + else: + properties = ("x", "y", "width", "height") + + # Check for valid properties. + for key in args.keys(): + if key not in properties: + warn(f"Property '{key}' not supported in layout options") + return {} + + # Set the layout properties. + for prop in properties: + if prop not in args.keys(): + warn(f"Property '{prop}' must be specified in layout options") + return {} + + value = args[prop] + + try: + float(value) + except ValueError: + warn(f"Property '{prop}' value '{value}' must be numeric in layout") + return {} + + if value < 0 or value > 1: + warn( + f"Property '{prop}' value '{value}' must be in range " + f"0 < x <= 1 in layout options" + ) + return {} + + # Convert to the format used by Excel for easier testing + layout[prop] = f"{value:.17g}" + + return layout + + def _get_points_properties(self, user_points): + # Convert user points properties to structure required internally. + points = [] + + if not user_points: + return [] + + for user_point in user_points: + point = {} + + if user_point is not None: + # Set the line properties for the point. + line = Shape._get_line_properties(user_point.get("line")) + + # Allow 'border' as a synonym for 'line'. + if "border" in user_point: + line = Shape._get_line_properties(user_point["border"]) + + # Set the fill properties for the chartarea. + fill = Shape._get_fill_properties(user_point.get("fill")) + + # Set the pattern fill properties for the series. + pattern = Shape._get_pattern_properties(user_point.get("pattern")) + + # Set the gradient fill properties for the series. + gradient = Shape._get_gradient_properties(user_point.get("gradient")) + + # Pattern fill overrides solid fill. + if pattern: + self.fill = None + + # Gradient fill overrides the solid and pattern fill. + if gradient: + pattern = None + fill = None + + point["line"] = line + point["fill"] = fill + point["pattern"] = pattern + point["gradient"] = gradient + + points.append(point) + + return points + + def _has_fill_formatting(self, element): + # Check if a chart element has line, fill or gradient formatting. + has_fill = False + has_line = False + has_pattern = element.get("pattern") + has_gradient = element.get("gradient") + + if element.get("fill") and element["fill"]["defined"]: + has_fill = True + + if element.get("line") and element["line"]["defined"]: + has_line = True + + return has_fill or has_line or has_pattern or has_gradient + + def _get_display_units(self, display_units): + # Convert user defined display units to internal units. + if not display_units: + return None + + types = { + "hundreds": "hundreds", + "thousands": "thousands", + "ten_thousands": "tenThousands", + "hundred_thousands": "hundredThousands", + "millions": "millions", + "ten_millions": "tenMillions", + "hundred_millions": "hundredMillions", + "billions": "billions", + "trillions": "trillions", + } + + if display_units in types: + display_units = types[display_units] + else: + warn(f"Unknown display_units type '{display_units}'") + return None + + return display_units + + def _get_tick_type(self, tick_type): + # Convert user defined display units to internal units. + if not tick_type: + return None + + types = { + "outside": "out", + "inside": "in", + "none": "none", + "cross": "cross", + } + + if tick_type in types: + tick_type = types[tick_type] + else: + warn(f"Unknown tick_type '{tick_type}'") + return None + + return tick_type + + def _get_primary_axes_series(self): + # Returns series which use the primary axes. + primary_axes_series = [] + + for series in self.series: + if not series["y2_axis"]: + primary_axes_series.append(series) + + return primary_axes_series + + def _get_secondary_axes_series(self): + # Returns series which use the secondary axes. + secondary_axes_series = [] + + for series in self.series: + if series["y2_axis"]: + secondary_axes_series.append(series) + + return secondary_axes_series + + def _add_axis_ids(self, args): + # Add unique ids for primary or secondary axes + chart_id = 5001 + int(self.id) + axis_count = 1 + len(self.axis2_ids) + len(self.axis_ids) + + id1 = f"{chart_id:04d}{axis_count:04d}" + id2 = f"{chart_id:04d}{axis_count + 1:04d}" + + if args["primary_axes"]: + self.axis_ids.append(id1) + self.axis_ids.append(id2) + + if not args["primary_axes"]: + self.axis2_ids.append(id1) + self.axis2_ids.append(id2) + + def _set_default_properties(self): + # Setup the default properties for a chart. + + self.x_axis["defaults"] = { + "num_format": "General", + "major_gridlines": {"visible": 0}, + } + + self.y_axis["defaults"] = { + "num_format": "General", + "major_gridlines": {"visible": 1}, + } + + self.x2_axis["defaults"] = { + "num_format": "General", + "label_position": "none", + "crossing": "max", + "visible": 0, + } + + self.y2_axis["defaults"] = { + "num_format": "General", + "major_gridlines": {"visible": 0}, + "position": "right", + "visible": 1, + } + + self.set_x_axis({}) + self.set_y_axis({}) + + self.set_x2_axis({}) + self.set_y2_axis({}) + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_chart_space(self): + # Write the <c:chartSpace> element. + schema = "http://schemas.openxmlformats.org/" + xmlns_c = schema + "drawingml/2006/chart" + xmlns_a = schema + "drawingml/2006/main" + xmlns_r = schema + "officeDocument/2006/relationships" + + attributes = [ + ("xmlns:c", xmlns_c), + ("xmlns:a", xmlns_a), + ("xmlns:r", xmlns_r), + ] + + self._xml_start_tag("c:chartSpace", attributes) + + def _write_lang(self): + # Write the <c:lang> element. + val = "en-US" + + attributes = [("val", val)] + + self._xml_empty_tag("c:lang", attributes) + + def _write_style(self): + # Write the <c:style> element. + style_id = self.style_id + + # Don't write an element for the default style, 2. + if style_id == 2: + return + + attributes = [("val", style_id)] + + self._xml_empty_tag("c:style", attributes) + + def _write_chart(self): + # Write the <c:chart> element. + self._xml_start_tag("c:chart") + + if self.title_none: + # Turn off the title. + self._write_c_auto_title_deleted() + else: + # Write the chart title elements. + if self.title_formula is not None: + self._write_title_formula( + self.title_formula, + self.title_data_id, + None, + self.title_font, + self.title_layout, + self.title_overlay, + ) + elif self.title_name is not None: + self._write_title_rich( + self.title_name, + None, + self.title_font, + self.title_layout, + self.title_overlay, + ) + + # Write the c:plotArea element. + self._write_plot_area() + + # Write the c:legend element. + self._write_legend() + + # Write the c:plotVisOnly element. + self._write_plot_vis_only() + + # Write the c:dispBlanksAs element. + self._write_disp_blanks_as() + + # Write the c:extLst element. + if self.show_na_as_empty: + self._write_c_ext_lst_display_na() + + self._xml_end_tag("c:chart") + + def _write_disp_blanks_as(self): + # Write the <c:dispBlanksAs> element. + val = self.show_blanks + + # Ignore the default value. + if val == "gap": + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:dispBlanksAs", attributes) + + def _write_plot_area(self): + # Write the <c:plotArea> element. + self._xml_start_tag("c:plotArea") + + # Write the c:layout element. + self._write_layout(self.plotarea.get("layout"), "plot") + + # Write subclass chart type elements for primary and secondary axes. + self._write_chart_type({"primary_axes": True}) + self._write_chart_type({"primary_axes": False}) + + # Configure a combined chart if present. + second_chart = self.combined + if second_chart: + # Secondary axis has unique id otherwise use same as primary. + if second_chart.is_secondary: + second_chart.id = 1000 + self.id + else: + second_chart.id = self.id + + # Share the same filehandle for writing. + second_chart.fh = self.fh + + # Share series index with primary chart. + second_chart.series_index = self.series_index + + # Write the subclass chart type elements for combined chart. + second_chart._write_chart_type({"primary_axes": True}) + second_chart._write_chart_type({"primary_axes": False}) + + # Write the category and value elements for the primary axes. + args = {"x_axis": self.x_axis, "y_axis": self.y_axis, "axis_ids": self.axis_ids} + + if self.date_category: + self._write_date_axis(args) + else: + self._write_cat_axis(args) + + self._write_val_axis(args) + + # Write the category and value elements for the secondary axes. + args = { + "x_axis": self.x2_axis, + "y_axis": self.y2_axis, + "axis_ids": self.axis2_ids, + } + + self._write_val_axis(args) + + # Write the secondary axis for the secondary chart. + if second_chart and second_chart.is_secondary: + args = { + "x_axis": second_chart.x2_axis, + "y_axis": second_chart.y2_axis, + "axis_ids": second_chart.axis2_ids, + } + + second_chart._write_val_axis(args) + + if self.date_category: + self._write_date_axis(args) + else: + self._write_cat_axis(args) + + # Write the c:dTable element. + self._write_d_table() + + # Write the c:spPr element for the plotarea formatting. + self._write_sp_pr(self.plotarea) + + self._xml_end_tag("c:plotArea") + + def _write_layout(self, layout, layout_type): + # Write the <c:layout> element. + + if not layout: + # Automatic layout. + self._xml_empty_tag("c:layout") + else: + # User defined manual layout. + self._xml_start_tag("c:layout") + self._write_manual_layout(layout, layout_type) + self._xml_end_tag("c:layout") + + def _write_manual_layout(self, layout, layout_type): + # Write the <c:manualLayout> element. + self._xml_start_tag("c:manualLayout") + + # Plotarea has a layoutTarget element. + if layout_type == "plot": + self._xml_empty_tag("c:layoutTarget", [("val", "inner")]) + + # Set the x, y positions. + self._xml_empty_tag("c:xMode", [("val", "edge")]) + self._xml_empty_tag("c:yMode", [("val", "edge")]) + self._xml_empty_tag("c:x", [("val", layout["x"])]) + self._xml_empty_tag("c:y", [("val", layout["y"])]) + + # For plotarea and legend set the width and height. + if layout_type != "text": + self._xml_empty_tag("c:w", [("val", layout["width"])]) + self._xml_empty_tag("c:h", [("val", layout["height"])]) + + self._xml_end_tag("c:manualLayout") + + def _write_chart_type(self, args): + # pylint: disable=unused-argument + # Write the chart type element. This method should be overridden + # by the subclasses. + return + + def _write_grouping(self, val): + # Write the <c:grouping> element. + attributes = [("val", val)] + + self._xml_empty_tag("c:grouping", attributes) + + def _write_series(self, series): + # Write the series elements. + self._write_ser(series) + + def _write_ser(self, series): + # Write the <c:ser> element. + index = self.series_index + self.series_index += 1 + + self._xml_start_tag("c:ser") + + # Write the c:idx element. + self._write_idx(index) + + # Write the c:order element. + self._write_order(index) + + # Write the series name. + self._write_series_name(series) + + # Write the c:spPr element. + self._write_sp_pr(series) + + # Write the c:marker element. + self._write_marker(series["marker"]) + + # Write the c:invertIfNegative element. + self._write_c_invert_if_negative(series["invert_if_neg"]) + + # Write the c:dPt element. + self._write_d_pt(series["points"]) + + # Write the c:dLbls element. + self._write_d_lbls(series["labels"]) + + # Write the c:trendline element. + self._write_trendline(series["trendline"]) + + # Write the c:errBars element. + self._write_error_bars(series["error_bars"]) + + # Write the c:cat element. + self._write_cat(series) + + # Write the c:val element. + self._write_val(series) + + # Write the c:smooth element. + if self.smooth_allowed: + self._write_c_smooth(series["smooth"]) + + # Write the c:extLst element. + if series.get("inverted_color"): + self._write_c_ext_lst_inverted_color(series["inverted_color"]) + + self._xml_end_tag("c:ser") + + def _write_c_ext_lst_inverted_color(self, color): + # Write the <c:extLst> element for the inverted fill color. + + uri = "{6F2FDCE9-48DA-4B69-8628-5D25D57E5C99}" + xmlns_c_14 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" + + attributes1 = [ + ("uri", uri), + ("xmlns:c14", xmlns_c_14), + ] + + attributes2 = [("xmlns:c14", xmlns_c_14)] + + self._xml_start_tag("c:extLst") + self._xml_start_tag("c:ext", attributes1) + self._xml_start_tag("c14:invertSolidFillFmt") + self._xml_start_tag("c14:spPr", attributes2) + + self._write_a_solid_fill({"color": color}) + + self._xml_end_tag("c14:spPr") + self._xml_end_tag("c14:invertSolidFillFmt") + self._xml_end_tag("c:ext") + self._xml_end_tag("c:extLst") + + def _write_c_ext_lst_display_na(self): + # Write the <c:extLst> element for the display NA as empty cell option. + + uri = "{56B9EC1D-385E-4148-901F-78D8002777C0}" + xmlns_c_16 = "http://schemas.microsoft.com/office/drawing/2017/03/chart" + + attributes1 = [ + ("uri", uri), + ("xmlns:c16r3", xmlns_c_16), + ] + + attributes2 = [("val", 1)] + + self._xml_start_tag("c:extLst") + self._xml_start_tag("c:ext", attributes1) + self._xml_start_tag("c16r3:dataDisplayOptions16") + self._xml_empty_tag("c16r3:dispNaAsBlank", attributes2) + self._xml_end_tag("c16r3:dataDisplayOptions16") + self._xml_end_tag("c:ext") + self._xml_end_tag("c:extLst") + + def _write_idx(self, val): + # Write the <c:idx> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:idx", attributes) + + def _write_order(self, val): + # Write the <c:order> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:order", attributes) + + def _write_series_name(self, series): + # Write the series name. + + if series["name_formula"] is not None: + self._write_tx_formula(series["name_formula"], series["name_id"]) + elif series["name"] is not None: + self._write_tx_value(series["name"]) + + def _write_c_smooth(self, smooth): + # Write the <c:smooth> element. + + if smooth: + self._xml_empty_tag("c:smooth", [("val", "1")]) + + def _write_cat(self, series): + # Write the <c:cat> element. + formula = series["categories"] + data_id = series["cat_data_id"] + data = None + + if data_id is not None: + data = self.formula_data[data_id] + + # Ignore <c:cat> elements for charts without category values. + if not formula: + return + + self._xml_start_tag("c:cat") + + # Check the type of cached data. + cat_type = self._get_data_type(data) + + if cat_type == "str": + self.cat_has_num_fmt = 0 + # Write the c:numRef element. + self._write_str_ref(formula, data, cat_type) + + elif cat_type == "multi_str": + self.cat_has_num_fmt = 0 + # Write the c:numRef element. + self._write_multi_lvl_str_ref(formula, data) + + else: + self.cat_has_num_fmt = 1 + # Write the c:numRef element. + self._write_num_ref(formula, data, cat_type) + + self._xml_end_tag("c:cat") + + def _write_val(self, series): + # Write the <c:val> element. + formula = series["values"] + data_id = series["val_data_id"] + data = self.formula_data[data_id] + + self._xml_start_tag("c:val") + + # Unlike Cat axes data should only be numeric. + # Write the c:numRef element. + self._write_num_ref(formula, data, "num") + + self._xml_end_tag("c:val") + + def _write_num_ref(self, formula, data, ref_type): + # Write the <c:numRef> element. + self._xml_start_tag("c:numRef") + + # Write the c:f element. + self._write_series_formula(formula) + + if ref_type == "num": + # Write the c:numCache element. + self._write_num_cache(data) + elif ref_type == "str": + # Write the c:strCache element. + self._write_str_cache(data) + + self._xml_end_tag("c:numRef") + + def _write_str_ref(self, formula, data, ref_type): + # Write the <c:strRef> element. + + self._xml_start_tag("c:strRef") + + # Write the c:f element. + self._write_series_formula(formula) + + if ref_type == "num": + # Write the c:numCache element. + self._write_num_cache(data) + elif ref_type == "str": + # Write the c:strCache element. + self._write_str_cache(data) + + self._xml_end_tag("c:strRef") + + def _write_multi_lvl_str_ref(self, formula, data): + # Write the <c:multiLvlStrRef> element. + + if not data: + return + + self._xml_start_tag("c:multiLvlStrRef") + + # Write the c:f element. + self._write_series_formula(formula) + + self._xml_start_tag("c:multiLvlStrCache") + + # Write the c:ptCount element. + count = len(data[-1]) + self._write_pt_count(count) + + for cat_data in reversed(data): + self._xml_start_tag("c:lvl") + + for i, point in enumerate(cat_data): + # Write the c:pt element. + self._write_pt(i, point) + + self._xml_end_tag("c:lvl") + + self._xml_end_tag("c:multiLvlStrCache") + self._xml_end_tag("c:multiLvlStrRef") + + def _write_series_formula(self, formula): + # Write the <c:f> element. + + # Strip the leading '=' from the formula. + if formula.startswith("="): + formula = formula.lstrip("=") + + self._xml_data_element("c:f", formula) + + def _write_axis_ids(self, args): + # Write the <c:axId> elements for the primary or secondary axes. + + # Generate the axis ids. + self._add_axis_ids(args) + + if args["primary_axes"]: + # Write the axis ids for the primary axes. + self._write_axis_id(self.axis_ids[0]) + self._write_axis_id(self.axis_ids[1]) + else: + # Write the axis ids for the secondary axes. + self._write_axis_id(self.axis2_ids[0]) + self._write_axis_id(self.axis2_ids[1]) + + def _write_axis_id(self, val): + # Write the <c:axId> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:axId", attributes) + + def _write_cat_axis(self, args): + # Write the <c:catAx> element. Usually the X axis. + x_axis = args["x_axis"] + y_axis = args["y_axis"] + axis_ids = args["axis_ids"] + + # If there are no axis_ids then we don't need to write this element. + if axis_ids is None or not axis_ids: + return + + position = self.cat_axis_position + is_y_axis = self.horiz_cat_axis + + # Overwrite the default axis position with a user supplied value. + if x_axis.get("position"): + position = x_axis["position"] + + self._xml_start_tag("c:catAx") + + self._write_axis_id(axis_ids[0]) + + # Write the c:scaling element. + self._write_scaling(x_axis.get("reverse"), None, None, None) + + if not x_axis.get("visible"): + self._write_delete(1) + + # Write the c:axPos element. + self._write_axis_pos(position, y_axis.get("reverse")) + + # Write the c:majorGridlines element. + self._write_major_gridlines(x_axis.get("major_gridlines")) + + # Write the c:minorGridlines element. + self._write_minor_gridlines(x_axis.get("minor_gridlines")) + + # Write the axis title elements. + if x_axis["formula"] is not None: + self._write_title_formula( + x_axis["formula"], + x_axis["data_id"], + is_y_axis, + x_axis["name_font"], + x_axis["name_layout"], + ) + elif x_axis["name"] is not None: + self._write_title_rich( + x_axis["name"], is_y_axis, x_axis["name_font"], x_axis["name_layout"] + ) + + # Write the c:numFmt element. + self._write_cat_number_format(x_axis) + + # Write the c:majorTickMark element. + self._write_major_tick_mark(x_axis.get("major_tick_mark")) + + # Write the c:minorTickMark element. + self._write_minor_tick_mark(x_axis.get("minor_tick_mark")) + + # Write the c:tickLblPos element. + self._write_tick_label_pos(x_axis.get("label_position")) + + # Write the c:spPr element for the axis line. + self._write_sp_pr(x_axis) + + # Write the axis font elements. + self._write_axis_font(x_axis.get("num_font")) + + # Write the c:crossAx element. + self._write_cross_axis(axis_ids[1]) + + if self.show_crosses or x_axis.get("visible"): + # Note, the category crossing comes from the value axis. + if ( + y_axis.get("crossing") is None + or y_axis.get("crossing") == "max" + or y_axis["crossing"] == "min" + ): + # Write the c:crosses element. + self._write_crosses(y_axis.get("crossing")) + else: + # Write the c:crossesAt element. + self._write_c_crosses_at(y_axis.get("crossing")) + + # Write the c:auto element. + if not x_axis.get("text_axis"): + self._write_auto(1) + + # Write the c:labelAlign element. + self._write_label_align(x_axis.get("label_align")) + + # Write the c:labelOffset element. + self._write_label_offset(100) + + # Write the c:tickLblSkip element. + self._write_c_tick_lbl_skip(x_axis.get("interval_unit")) + + # Write the c:tickMarkSkip element. + self._write_c_tick_mark_skip(x_axis.get("interval_tick")) + + self._xml_end_tag("c:catAx") + + def _write_val_axis(self, args): + # Write the <c:valAx> element. Usually the Y axis. + x_axis = args["x_axis"] + y_axis = args["y_axis"] + axis_ids = args["axis_ids"] + position = args.get("position", self.val_axis_position) + is_y_axis = self.horiz_val_axis + + # If there are no axis_ids then we don't need to write this element. + if axis_ids is None or not axis_ids: + return + + # Overwrite the default axis position with a user supplied value. + position = y_axis.get("position") or position + + self._xml_start_tag("c:valAx") + + self._write_axis_id(axis_ids[1]) + + # Write the c:scaling element. + self._write_scaling( + y_axis.get("reverse"), + y_axis.get("min"), + y_axis.get("max"), + y_axis.get("log_base"), + ) + + if not y_axis.get("visible"): + self._write_delete(1) + + # Write the c:axPos element. + self._write_axis_pos(position, x_axis.get("reverse")) + + # Write the c:majorGridlines element. + self._write_major_gridlines(y_axis.get("major_gridlines")) + + # Write the c:minorGridlines element. + self._write_minor_gridlines(y_axis.get("minor_gridlines")) + + # Write the axis title elements. + if y_axis["formula"] is not None: + self._write_title_formula( + y_axis["formula"], + y_axis["data_id"], + is_y_axis, + y_axis["name_font"], + y_axis["name_layout"], + ) + elif y_axis["name"] is not None: + self._write_title_rich( + y_axis["name"], + is_y_axis, + y_axis.get("name_font"), + y_axis.get("name_layout"), + ) + + # Write the c:numberFormat element. + self._write_number_format(y_axis) + + # Write the c:majorTickMark element. + self._write_major_tick_mark(y_axis.get("major_tick_mark")) + + # Write the c:minorTickMark element. + self._write_minor_tick_mark(y_axis.get("minor_tick_mark")) + + # Write the c:tickLblPos element. + self._write_tick_label_pos(y_axis.get("label_position")) + + # Write the c:spPr element for the axis line. + self._write_sp_pr(y_axis) + + # Write the axis font elements. + self._write_axis_font(y_axis.get("num_font")) + + # Write the c:crossAx element. + self._write_cross_axis(axis_ids[0]) + + # Note, the category crossing comes from the value axis. + if ( + x_axis.get("crossing") is None + or x_axis["crossing"] == "max" + or x_axis["crossing"] == "min" + ): + # Write the c:crosses element. + self._write_crosses(x_axis.get("crossing")) + else: + # Write the c:crossesAt element. + self._write_c_crosses_at(x_axis.get("crossing")) + + # Write the c:crossBetween element. + self._write_cross_between(x_axis.get("position_axis")) + + # Write the c:majorUnit element. + self._write_c_major_unit(y_axis.get("major_unit")) + + # Write the c:minorUnit element. + self._write_c_minor_unit(y_axis.get("minor_unit")) + + # Write the c:dispUnits element. + self._write_disp_units( + y_axis.get("display_units"), y_axis.get("display_units_visible") + ) + + self._xml_end_tag("c:valAx") + + def _write_cat_val_axis(self, args): + # Write the <c:valAx> element. This is for the second valAx + # in scatter plots. Usually the X axis. + x_axis = args["x_axis"] + y_axis = args["y_axis"] + axis_ids = args["axis_ids"] + position = args["position"] or self.val_axis_position + is_y_axis = self.horiz_val_axis + + # If there are no axis_ids then we don't need to write this element. + if axis_ids is None or not axis_ids: + return + + # Overwrite the default axis position with a user supplied value. + position = x_axis.get("position") or position + + self._xml_start_tag("c:valAx") + + self._write_axis_id(axis_ids[0]) + + # Write the c:scaling element. + self._write_scaling( + x_axis.get("reverse"), + x_axis.get("min"), + x_axis.get("max"), + x_axis.get("log_base"), + ) + + if not x_axis.get("visible"): + self._write_delete(1) + + # Write the c:axPos element. + self._write_axis_pos(position, y_axis.get("reverse")) + + # Write the c:majorGridlines element. + self._write_major_gridlines(x_axis.get("major_gridlines")) + + # Write the c:minorGridlines element. + self._write_minor_gridlines(x_axis.get("minor_gridlines")) + + # Write the axis title elements. + if x_axis["formula"] is not None: + self._write_title_formula( + x_axis["formula"], + x_axis["data_id"], + is_y_axis, + x_axis["name_font"], + x_axis["name_layout"], + ) + elif x_axis["name"] is not None: + self._write_title_rich( + x_axis["name"], is_y_axis, x_axis["name_font"], x_axis["name_layout"] + ) + + # Write the c:numberFormat element. + self._write_number_format(x_axis) + + # Write the c:majorTickMark element. + self._write_major_tick_mark(x_axis.get("major_tick_mark")) + + # Write the c:minorTickMark element. + self._write_minor_tick_mark(x_axis.get("minor_tick_mark")) + + # Write the c:tickLblPos element. + self._write_tick_label_pos(x_axis.get("label_position")) + + # Write the c:spPr element for the axis line. + self._write_sp_pr(x_axis) + + # Write the axis font elements. + self._write_axis_font(x_axis.get("num_font")) + + # Write the c:crossAx element. + self._write_cross_axis(axis_ids[1]) + + # Note, the category crossing comes from the value axis. + if ( + y_axis.get("crossing") is None + or y_axis["crossing"] == "max" + or y_axis["crossing"] == "min" + ): + # Write the c:crosses element. + self._write_crosses(y_axis.get("crossing")) + else: + # Write the c:crossesAt element. + self._write_c_crosses_at(y_axis.get("crossing")) + + # Write the c:crossBetween element. + self._write_cross_between(y_axis.get("position_axis")) + + # Write the c:majorUnit element. + self._write_c_major_unit(x_axis.get("major_unit")) + + # Write the c:minorUnit element. + self._write_c_minor_unit(x_axis.get("minor_unit")) + + # Write the c:dispUnits element. + self._write_disp_units( + x_axis.get("display_units"), x_axis.get("display_units_visible") + ) + + self._xml_end_tag("c:valAx") + + def _write_date_axis(self, args): + # Write the <c:dateAx> element. Usually the X axis. + x_axis = args["x_axis"] + y_axis = args["y_axis"] + axis_ids = args["axis_ids"] + + # If there are no axis_ids then we don't need to write this element. + if axis_ids is None or not axis_ids: + return + + position = self.cat_axis_position + + # Overwrite the default axis position with a user supplied value. + position = x_axis.get("position") or position + + self._xml_start_tag("c:dateAx") + + self._write_axis_id(axis_ids[0]) + + # Write the c:scaling element. + self._write_scaling( + x_axis.get("reverse"), + x_axis.get("min"), + x_axis.get("max"), + x_axis.get("log_base"), + ) + + if not x_axis.get("visible"): + self._write_delete(1) + + # Write the c:axPos element. + self._write_axis_pos(position, y_axis.get("reverse")) + + # Write the c:majorGridlines element. + self._write_major_gridlines(x_axis.get("major_gridlines")) + + # Write the c:minorGridlines element. + self._write_minor_gridlines(x_axis.get("minor_gridlines")) + + # Write the axis title elements. + if x_axis["formula"] is not None: + self._write_title_formula( + x_axis["formula"], + x_axis["data_id"], + None, + x_axis["name_font"], + x_axis["name_layout"], + ) + elif x_axis["name"] is not None: + self._write_title_rich( + x_axis["name"], None, x_axis["name_font"], x_axis["name_layout"] + ) + + # Write the c:numFmt element. + self._write_number_format(x_axis) + + # Write the c:majorTickMark element. + self._write_major_tick_mark(x_axis.get("major_tick_mark")) + + # Write the c:minorTickMark element. + self._write_minor_tick_mark(x_axis.get("minor_tick_mark")) + + # Write the c:tickLblPos element. + self._write_tick_label_pos(x_axis.get("label_position")) + + # Write the c:spPr element for the axis line. + self._write_sp_pr(x_axis) + + # Write the axis font elements. + self._write_axis_font(x_axis.get("num_font")) + + # Write the c:crossAx element. + self._write_cross_axis(axis_ids[1]) + + if self.show_crosses or x_axis.get("visible"): + # Note, the category crossing comes from the value axis. + if ( + y_axis.get("crossing") is None + or y_axis.get("crossing") == "max" + or y_axis["crossing"] == "min" + ): + # Write the c:crosses element. + self._write_crosses(y_axis.get("crossing")) + else: + # Write the c:crossesAt element. + self._write_c_crosses_at(y_axis.get("crossing")) + + # Write the c:auto element. + self._write_auto(1) + + # Write the c:labelOffset element. + self._write_label_offset(100) + + # Write the c:tickLblSkip element. + self._write_c_tick_lbl_skip(x_axis.get("interval_unit")) + + # Write the c:tickMarkSkip element. + self._write_c_tick_mark_skip(x_axis.get("interval_tick")) + + # Write the c:majorUnit element. + self._write_c_major_unit(x_axis.get("major_unit")) + + # Write the c:majorTimeUnit element. + if x_axis.get("major_unit"): + self._write_c_major_time_unit(x_axis["major_unit_type"]) + + # Write the c:minorUnit element. + self._write_c_minor_unit(x_axis.get("minor_unit")) + + # Write the c:minorTimeUnit element. + if x_axis.get("minor_unit"): + self._write_c_minor_time_unit(x_axis["minor_unit_type"]) + + self._xml_end_tag("c:dateAx") + + def _write_scaling(self, reverse, min_val, max_val, log_base): + # Write the <c:scaling> element. + + self._xml_start_tag("c:scaling") + + # Write the c:logBase element. + self._write_c_log_base(log_base) + + # Write the c:orientation element. + self._write_orientation(reverse) + + # Write the c:max element. + self._write_c_max(max_val) + + # Write the c:min element. + self._write_c_min(min_val) + + self._xml_end_tag("c:scaling") + + def _write_c_log_base(self, val): + # Write the <c:logBase> element. + + if not val: + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:logBase", attributes) + + def _write_orientation(self, reverse): + # Write the <c:orientation> element. + val = "minMax" + + if reverse: + val = "maxMin" + + attributes = [("val", val)] + + self._xml_empty_tag("c:orientation", attributes) + + def _write_c_max(self, max_val): + # Write the <c:max> element. + + if max_val is None: + return + + attributes = [("val", max_val)] + + self._xml_empty_tag("c:max", attributes) + + def _write_c_min(self, min_val): + # Write the <c:min> element. + + if min_val is None: + return + + attributes = [("val", min_val)] + + self._xml_empty_tag("c:min", attributes) + + def _write_axis_pos(self, val, reverse): + # Write the <c:axPos> element. + + if reverse: + if val == "l": + val = "r" + if val == "b": + val = "t" + + attributes = [("val", val)] + + self._xml_empty_tag("c:axPos", attributes) + + def _write_number_format(self, axis): + # Write the <c:numberFormat> element. Note: It is assumed that if + # a user defined number format is supplied (i.e., non-default) then + # the sourceLinked attribute is 0. + # The user can override this if required. + format_code = axis.get("num_format") + source_linked = 1 + + # Check if a user defined number format has been set. + if format_code is not None and format_code != axis["defaults"]["num_format"]: + source_linked = 0 + + # User override of sourceLinked. + if axis.get("num_format_linked"): + source_linked = 1 + + attributes = [ + ("formatCode", format_code), + ("sourceLinked", source_linked), + ] + + self._xml_empty_tag("c:numFmt", attributes) + + def _write_cat_number_format(self, axis): + # Write the <c:numFmt> element. Special case handler for category + # axes which don't always have a number format. + format_code = axis.get("num_format") + source_linked = 1 + default_format = 1 + + # Check if a user defined number format has been set. + if format_code is not None and format_code != axis["defaults"]["num_format"]: + source_linked = 0 + default_format = 0 + + # User override of sourceLinked. + if axis.get("num_format_linked"): + source_linked = 1 + + # Skip if cat doesn't have a num format (unless it is non-default). + if not self.cat_has_num_fmt and default_format: + return + + attributes = [ + ("formatCode", format_code), + ("sourceLinked", source_linked), + ] + + self._xml_empty_tag("c:numFmt", attributes) + + def _write_data_label_number_format(self, format_code): + # Write the <c:numberFormat> element for data labels. + source_linked = 0 + + attributes = [ + ("formatCode", format_code), + ("sourceLinked", source_linked), + ] + + self._xml_empty_tag("c:numFmt", attributes) + + def _write_major_tick_mark(self, val): + # Write the <c:majorTickMark> element. + + if not val: + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:majorTickMark", attributes) + + def _write_minor_tick_mark(self, val): + # Write the <c:minorTickMark> element. + + if not val: + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:minorTickMark", attributes) + + def _write_tick_label_pos(self, val=None): + # Write the <c:tickLblPos> element. + if val is None or val == "next_to": + val = "nextTo" + + attributes = [("val", val)] + + self._xml_empty_tag("c:tickLblPos", attributes) + + def _write_cross_axis(self, val): + # Write the <c:crossAx> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:crossAx", attributes) + + def _write_crosses(self, val=None): + # Write the <c:crosses> element. + if val is None: + val = "autoZero" + + attributes = [("val", val)] + + self._xml_empty_tag("c:crosses", attributes) + + def _write_c_crosses_at(self, val): + # Write the <c:crossesAt> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:crossesAt", attributes) + + def _write_auto(self, val): + # Write the <c:auto> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:auto", attributes) + + def _write_label_align(self, val=None): + # Write the <c:labelAlign> element. + + if val is None: + val = "ctr" + + if val == "right": + val = "r" + + if val == "left": + val = "l" + + attributes = [("val", val)] + + self._xml_empty_tag("c:lblAlgn", attributes) + + def _write_label_offset(self, val): + # Write the <c:labelOffset> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:lblOffset", attributes) + + def _write_c_tick_lbl_skip(self, val): + # Write the <c:tickLblSkip> element. + if val is None: + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:tickLblSkip", attributes) + + def _write_c_tick_mark_skip(self, val): + # Write the <c:tickMarkSkip> element. + if val is None: + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:tickMarkSkip", attributes) + + def _write_major_gridlines(self, gridlines): + # Write the <c:majorGridlines> element. + + if not gridlines: + return + + if not gridlines["visible"]: + return + + if gridlines["line"]["defined"]: + self._xml_start_tag("c:majorGridlines") + + # Write the c:spPr element. + self._write_sp_pr(gridlines) + + self._xml_end_tag("c:majorGridlines") + else: + self._xml_empty_tag("c:majorGridlines") + + def _write_minor_gridlines(self, gridlines): + # Write the <c:minorGridlines> element. + + if not gridlines: + return + + if not gridlines["visible"]: + return + + if gridlines["line"]["defined"]: + self._xml_start_tag("c:minorGridlines") + + # Write the c:spPr element. + self._write_sp_pr(gridlines) + + self._xml_end_tag("c:minorGridlines") + else: + self._xml_empty_tag("c:minorGridlines") + + def _write_cross_between(self, val): + # Write the <c:crossBetween> element. + if val is None: + val = self.cross_between + + attributes = [("val", val)] + + self._xml_empty_tag("c:crossBetween", attributes) + + def _write_c_major_unit(self, val): + # Write the <c:majorUnit> element. + + if not val: + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:majorUnit", attributes) + + def _write_c_minor_unit(self, val): + # Write the <c:minorUnit> element. + + if not val: + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:minorUnit", attributes) + + def _write_c_major_time_unit(self, val=None): + # Write the <c:majorTimeUnit> element. + if val is None: + val = "days" + + attributes = [("val", val)] + + self._xml_empty_tag("c:majorTimeUnit", attributes) + + def _write_c_minor_time_unit(self, val=None): + # Write the <c:minorTimeUnit> element. + if val is None: + val = "days" + + attributes = [("val", val)] + + self._xml_empty_tag("c:minorTimeUnit", attributes) + + def _write_legend(self): + # Write the <c:legend> element. + legend = self.legend + position = legend.get("position", "right") + font = legend.get("font") + delete_series = [] + overlay = 0 + + if legend.get("delete_series") and isinstance(legend["delete_series"], list): + delete_series = legend["delete_series"] + + if position.startswith("overlay_"): + position = position.replace("overlay_", "") + overlay = 1 + + allowed = { + "right": "r", + "left": "l", + "top": "t", + "bottom": "b", + "top_right": "tr", + } + + if position == "none": + return + + if position not in allowed: + return + + position = allowed[position] + + self._xml_start_tag("c:legend") + + # Write the c:legendPos element. + self._write_legend_pos(position) + + # Remove series labels from the legend. + for index in delete_series: + # Write the c:legendEntry element. + self._write_legend_entry(index) + + # Write the c:layout element. + self._write_layout(legend.get("layout"), "legend") + + # Write the c:overlay element. + if overlay: + self._write_overlay() + + if font: + self._write_tx_pr(font) + + # Write the c:spPr element. + self._write_sp_pr(legend) + + self._xml_end_tag("c:legend") + + def _write_legend_pos(self, val): + # Write the <c:legendPos> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:legendPos", attributes) + + def _write_legend_entry(self, index): + # Write the <c:legendEntry> element. + + self._xml_start_tag("c:legendEntry") + + # Write the c:idx element. + self._write_idx(index) + + # Write the c:delete element. + self._write_delete(1) + + self._xml_end_tag("c:legendEntry") + + def _write_overlay(self): + # Write the <c:overlay> element. + val = 1 + + attributes = [("val", val)] + + self._xml_empty_tag("c:overlay", attributes) + + def _write_plot_vis_only(self): + # Write the <c:plotVisOnly> element. + val = 1 + + # Ignore this element if we are plotting hidden data. + if self.show_hidden: + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:plotVisOnly", attributes) + + def _write_print_settings(self): + # Write the <c:printSettings> element. + self._xml_start_tag("c:printSettings") + + # Write the c:headerFooter element. + self._write_header_footer() + + # Write the c:pageMargins element. + self._write_page_margins() + + # Write the c:pageSetup element. + self._write_page_setup() + + self._xml_end_tag("c:printSettings") + + def _write_header_footer(self): + # Write the <c:headerFooter> element. + self._xml_empty_tag("c:headerFooter") + + def _write_page_margins(self): + # Write the <c:pageMargins> element. + bottom = 0.75 + left = 0.7 + right = 0.7 + top = 0.75 + header = 0.3 + footer = 0.3 + + attributes = [ + ("b", bottom), + ("l", left), + ("r", right), + ("t", top), + ("header", header), + ("footer", footer), + ] + + self._xml_empty_tag("c:pageMargins", attributes) + + def _write_page_setup(self): + # Write the <c:pageSetup> element. + self._xml_empty_tag("c:pageSetup") + + def _write_c_auto_title_deleted(self): + # Write the <c:autoTitleDeleted> element. + self._xml_empty_tag("c:autoTitleDeleted", [("val", 1)]) + + def _write_title_rich(self, title, is_y_axis, font, layout, overlay=False): + # Write the <c:title> element for a rich string. + + self._xml_start_tag("c:title") + + # Write the c:tx element. + self._write_tx_rich(title, is_y_axis, font) + + # Write the c:layout element. + self._write_layout(layout, "text") + + # Write the c:overlay element. + if overlay: + self._write_overlay() + + self._xml_end_tag("c:title") + + def _write_title_formula( + self, title, data_id, is_y_axis, font, layout, overlay=False + ): + # Write the <c:title> element for a rich string. + + self._xml_start_tag("c:title") + + # Write the c:tx element. + self._write_tx_formula(title, data_id) + + # Write the c:layout element. + self._write_layout(layout, "text") + + # Write the c:overlay element. + if overlay: + self._write_overlay() + + # Write the c:txPr element. + self._write_tx_pr(font, is_y_axis) + + self._xml_end_tag("c:title") + + def _write_tx_rich(self, title, is_y_axis, font): + # Write the <c:tx> element. + + self._xml_start_tag("c:tx") + + # Write the c:rich element. + self._write_rich(title, font, is_y_axis, ignore_rich_pr=False) + + self._xml_end_tag("c:tx") + + def _write_tx_value(self, title): + # Write the <c:tx> element with a value such as for series names. + + self._xml_start_tag("c:tx") + + # Write the c:v element. + self._write_v(title) + + self._xml_end_tag("c:tx") + + def _write_tx_formula(self, title, data_id): + # Write the <c:tx> element. + data = None + + if data_id is not None: + data = self.formula_data[data_id] + + self._xml_start_tag("c:tx") + + # Write the c:strRef element. + self._write_str_ref(title, data, "str") + + self._xml_end_tag("c:tx") + + def _write_rich(self, title, font, is_y_axis, ignore_rich_pr): + # Write the <c:rich> element. + + if font and font.get("rotation") is not None: + rotation = font["rotation"] + else: + rotation = None + + self._xml_start_tag("c:rich") + + # Write the a:bodyPr element. + self._write_a_body_pr(rotation, is_y_axis) + + # Write the a:lstStyle element. + self._write_a_lst_style() + + # Write the a:p element. + self._write_a_p_rich(title, font, ignore_rich_pr) + + self._xml_end_tag("c:rich") + + def _write_a_body_pr(self, rotation, is_y_axis): + # Write the <a:bodyPr> element. + attributes = [] + + if rotation is None and is_y_axis: + rotation = -5400000 + + if rotation is not None: + if rotation == 16200000: + # 270 deg/stacked angle. + attributes.append(("rot", 0)) + attributes.append(("vert", "wordArtVert")) + elif rotation == 16260000: + # 271 deg/East Asian vertical. + attributes.append(("rot", 0)) + attributes.append(("vert", "eaVert")) + else: + attributes.append(("rot", rotation)) + attributes.append(("vert", "horz")) + + self._xml_empty_tag("a:bodyPr", attributes) + + def _write_a_lst_style(self): + # Write the <a:lstStyle> element. + self._xml_empty_tag("a:lstStyle") + + def _write_a_p_rich(self, title, font, ignore_rich_pr): + # Write the <a:p> element for rich string titles. + + self._xml_start_tag("a:p") + + # Write the a:pPr element. + if not ignore_rich_pr: + self._write_a_p_pr_rich(font) + + # Write the a:r element. + self._write_a_r(title, font) + + self._xml_end_tag("a:p") + + def _write_a_p_formula(self, font): + # Write the <a:p> element for formula titles. + + self._xml_start_tag("a:p") + + # Write the a:pPr element. + self._write_a_p_pr_rich(font) + + # Write the a:endParaRPr element. + self._write_a_end_para_rpr() + + self._xml_end_tag("a:p") + + def _write_a_p_pr_rich(self, font): + # Write the <a:pPr> element for rich string titles. + + self._xml_start_tag("a:pPr") + + # Write the a:defRPr element. + self._write_a_def_rpr(font) + + self._xml_end_tag("a:pPr") + + def _write_a_def_rpr(self, font): + # Write the <a:defRPr> element. + has_color = 0 + + style_attributes = Shape._get_font_style_attributes(font) + latin_attributes = Shape._get_font_latin_attributes(font) + + if font and font.get("color") is not None: + has_color = 1 + + if latin_attributes or has_color: + self._xml_start_tag("a:defRPr", style_attributes) + + if has_color: + self._write_a_solid_fill({"color": font["color"]}) + + if latin_attributes: + self._write_a_latin(latin_attributes) + + self._xml_end_tag("a:defRPr") + else: + self._xml_empty_tag("a:defRPr", style_attributes) + + def _write_a_end_para_rpr(self): + # Write the <a:endParaRPr> element. + lang = "en-US" + + attributes = [("lang", lang)] + + self._xml_empty_tag("a:endParaRPr", attributes) + + def _write_a_r(self, title, font): + # Write the <a:r> element. + + self._xml_start_tag("a:r") + + # Write the a:rPr element. + self._write_a_r_pr(font) + + # Write the a:t element. + self._write_a_t(title) + + self._xml_end_tag("a:r") + + def _write_a_r_pr(self, font): + # Write the <a:rPr> element. + has_color = 0 + lang = "en-US" + + style_attributes = Shape._get_font_style_attributes(font) + latin_attributes = Shape._get_font_latin_attributes(font) + + if font and font["color"] is not None: + has_color = 1 + + # Add the lang type to the attributes. + style_attributes.insert(0, ("lang", lang)) + + if latin_attributes or has_color: + self._xml_start_tag("a:rPr", style_attributes) + + if has_color: + self._write_a_solid_fill({"color": font["color"]}) + + if latin_attributes: + self._write_a_latin(latin_attributes) + + self._xml_end_tag("a:rPr") + else: + self._xml_empty_tag("a:rPr", style_attributes) + + def _write_a_t(self, title): + # Write the <a:t> element. + + self._xml_data_element("a:t", title) + + def _write_tx_pr(self, font, is_y_axis=False): + # Write the <c:txPr> element. + + if font and font.get("rotation") is not None: + rotation = font["rotation"] + else: + rotation = None + + self._xml_start_tag("c:txPr") + + # Write the a:bodyPr element. + self._write_a_body_pr(rotation, is_y_axis) + + # Write the a:lstStyle element. + self._write_a_lst_style() + + # Write the a:p element. + self._write_a_p_formula(font) + + self._xml_end_tag("c:txPr") + + def _write_marker(self, marker): + # Write the <c:marker> element. + if marker is None: + marker = self.default_marker + + if not marker: + return + + if marker["type"] == "automatic": + return + + self._xml_start_tag("c:marker") + + # Write the c:symbol element. + self._write_symbol(marker["type"]) + + # Write the c:size element. + if marker.get("size"): + self._write_marker_size(marker["size"]) + + # Write the c:spPr element. + self._write_sp_pr(marker) + + self._xml_end_tag("c:marker") + + def _write_marker_size(self, val): + # Write the <c:size> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:size", attributes) + + def _write_symbol(self, val): + # Write the <c:symbol> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:symbol", attributes) + + def _write_sp_pr(self, series): + # Write the <c:spPr> element. + + if not self._has_fill_formatting(series): + return + + self._xml_start_tag("c:spPr") + + # Write the fill elements for solid charts such as pie and bar. + if series.get("fill") and series["fill"]["defined"]: + if "none" in series["fill"]: + # Write the a:noFill element. + self._write_a_no_fill() + else: + # Write the a:solidFill element. + self._write_a_solid_fill(series["fill"]) + + if series.get("pattern"): + # Write the a:gradFill element. + self._write_a_patt_fill(series["pattern"]) + + if series.get("gradient"): + # Write the a:gradFill element. + self._write_a_grad_fill(series["gradient"]) + + # Write the a:ln element. + if series.get("line") and series["line"]["defined"]: + self._write_a_ln(series["line"]) + + self._xml_end_tag("c:spPr") + + def _write_a_ln(self, line): + # Write the <a:ln> element. + attributes = [] + + # Add the line width as an attribute. + width = line.get("width") + + if width is not None: + # Round width to nearest 0.25, like Excel. + width = int((width + 0.125) * 4) / 4.0 + + # Convert to internal units. + width = int(0.5 + (12700 * width)) + + attributes = [("w", width)] + + if line.get("none") or line.get("color") or line.get("dash_type"): + self._xml_start_tag("a:ln", attributes) + + # Write the line fill. + if "none" in line: + # Write the a:noFill element. + self._write_a_no_fill() + elif "color" in line: + # Write the a:solidFill element. + self._write_a_solid_fill(line) + + # Write the line/dash type. + line_type = line.get("dash_type") + if line_type: + # Write the a:prstDash element. + self._write_a_prst_dash(line_type) + + self._xml_end_tag("a:ln") + else: + self._xml_empty_tag("a:ln", attributes) + + def _write_a_no_fill(self): + # Write the <a:noFill> element. + self._xml_empty_tag("a:noFill") + + def _write_a_solid_fill(self, fill): + # Write the <a:solidFill> element. + + self._xml_start_tag("a:solidFill") + + if "color" in fill: + color = _get_rgb_color(fill["color"]) + transparency = fill.get("transparency") + # Write the a:srgbClr element. + self._write_a_srgb_clr(color, transparency) + + self._xml_end_tag("a:solidFill") + + def _write_a_srgb_clr(self, val, transparency=None): + # Write the <a:srgbClr> element. + attributes = [("val", val)] + + if transparency: + self._xml_start_tag("a:srgbClr", attributes) + + # Write the a:alpha element. + self._write_a_alpha(transparency) + + self._xml_end_tag("a:srgbClr") + else: + self._xml_empty_tag("a:srgbClr", attributes) + + def _write_a_alpha(self, val): + # Write the <a:alpha> element. + + val = int((100 - int(val)) * 1000) + + attributes = [("val", val)] + + self._xml_empty_tag("a:alpha", attributes) + + def _write_a_prst_dash(self, val): + # Write the <a:prstDash> element. + + attributes = [("val", val)] + + self._xml_empty_tag("a:prstDash", attributes) + + def _write_trendline(self, trendline): + # Write the <c:trendline> element. + + if not trendline: + return + + self._xml_start_tag("c:trendline") + + # Write the c:name element. + self._write_name(trendline.get("name")) + + # Write the c:spPr element. + self._write_sp_pr(trendline) + + # Write the c:trendlineType element. + self._write_trendline_type(trendline["type"]) + + # Write the c:order element for polynomial trendlines. + if trendline["type"] == "poly": + self._write_trendline_order(trendline.get("order")) + + # Write the c:period element for moving average trendlines. + if trendline["type"] == "movingAvg": + self._write_period(trendline.get("period")) + + # Write the c:forward element. + self._write_forward(trendline.get("forward")) + + # Write the c:backward element. + self._write_backward(trendline.get("backward")) + + if "intercept" in trendline: + # Write the c:intercept element. + self._write_c_intercept(trendline["intercept"]) + + if trendline.get("display_r_squared"): + # Write the c:dispRSqr element. + self._write_c_disp_rsqr() + + if trendline.get("display_equation"): + # Write the c:dispEq element. + self._write_c_disp_eq() + + # Write the c:trendlineLbl element. + self._write_c_trendline_lbl(trendline) + + self._xml_end_tag("c:trendline") + + def _write_trendline_type(self, val): + # Write the <c:trendlineType> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:trendlineType", attributes) + + def _write_name(self, data): + # Write the <c:name> element. + + if data is None: + return + + self._xml_data_element("c:name", data) + + def _write_trendline_order(self, val): + # Write the <c:order> element. + val = max(val, 2) + + attributes = [("val", val)] + + self._xml_empty_tag("c:order", attributes) + + def _write_period(self, val): + # Write the <c:period> element. + val = max(val, 2) + + attributes = [("val", val)] + + self._xml_empty_tag("c:period", attributes) + + def _write_forward(self, val): + # Write the <c:forward> element. + + if not val: + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:forward", attributes) + + def _write_backward(self, val): + # Write the <c:backward> element. + + if not val: + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:backward", attributes) + + def _write_c_intercept(self, val): + # Write the <c:intercept> element. + attributes = [("val", val)] + + self._xml_empty_tag("c:intercept", attributes) + + def _write_c_disp_eq(self): + # Write the <c:dispEq> element. + attributes = [("val", 1)] + + self._xml_empty_tag("c:dispEq", attributes) + + def _write_c_disp_rsqr(self): + # Write the <c:dispRSqr> element. + attributes = [("val", 1)] + + self._xml_empty_tag("c:dispRSqr", attributes) + + def _write_c_trendline_lbl(self, trendline): + # Write the <c:trendlineLbl> element. + self._xml_start_tag("c:trendlineLbl") + + # Write the c:layout element. + self._write_layout(None, None) + + # Write the c:numFmt element. + self._write_trendline_num_fmt() + + # Write the c:spPr element. + self._write_sp_pr(trendline["label"]) + + # Write the data label font elements. + if trendline["label"]: + font = trendline["label"].get("font") + if font: + self._write_axis_font(font) + + self._xml_end_tag("c:trendlineLbl") + + def _write_trendline_num_fmt(self): + # Write the <c:numFmt> element. + attributes = [ + ("formatCode", "General"), + ("sourceLinked", 0), + ] + + self._xml_empty_tag("c:numFmt", attributes) + + def _write_hi_low_lines(self): + # Write the <c:hiLowLines> element. + hi_low_lines = self.hi_low_lines + + if hi_low_lines is None: + return + + if "line" in hi_low_lines and hi_low_lines["line"]["defined"]: + self._xml_start_tag("c:hiLowLines") + + # Write the c:spPr element. + self._write_sp_pr(hi_low_lines) + + self._xml_end_tag("c:hiLowLines") + else: + self._xml_empty_tag("c:hiLowLines") + + def _write_drop_lines(self): + # Write the <c:dropLines> element. + drop_lines = self.drop_lines + + if drop_lines is None: + return + + if drop_lines["line"]["defined"]: + self._xml_start_tag("c:dropLines") + + # Write the c:spPr element. + self._write_sp_pr(drop_lines) + + self._xml_end_tag("c:dropLines") + else: + self._xml_empty_tag("c:dropLines") + + def _write_overlap(self, val): + # Write the <c:overlap> element. + + if val is None: + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:overlap", attributes) + + def _write_num_cache(self, data): + # Write the <c:numCache> element. + if data: + count = len(data) + else: + count = 0 + + self._xml_start_tag("c:numCache") + + # Write the c:formatCode element. + self._write_format_code("General") + + # Write the c:ptCount element. + self._write_pt_count(count) + + for i in range(count): + token = data[i] + + if token is None: + continue + + try: + float(token) + except ValueError: + # Write non-numeric data as 0. + token = 0 + + # Write the c:pt element. + self._write_pt(i, token) + + self._xml_end_tag("c:numCache") + + def _write_str_cache(self, data): + # Write the <c:strCache> element. + count = len(data) + + self._xml_start_tag("c:strCache") + + # Write the c:ptCount element. + self._write_pt_count(count) + + for i in range(count): + # Write the c:pt element. + self._write_pt(i, data[i]) + + self._xml_end_tag("c:strCache") + + def _write_format_code(self, data): + # Write the <c:formatCode> element. + + self._xml_data_element("c:formatCode", data) + + def _write_pt_count(self, val): + # Write the <c:ptCount> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:ptCount", attributes) + + def _write_pt(self, idx, value): + # Write the <c:pt> element. + + if value is None: + return + + attributes = [("idx", idx)] + + self._xml_start_tag("c:pt", attributes) + + # Write the c:v element. + self._write_v(value) + + self._xml_end_tag("c:pt") + + def _write_v(self, data): + # Write the <c:v> element. + + self._xml_data_element("c:v", data) + + def _write_protection(self): + # Write the <c:protection> element. + if not self.protection: + return + + self._xml_empty_tag("c:protection") + + def _write_d_pt(self, points): + # Write the <c:dPt> elements. + index = -1 + + if not points: + return + + for point in points: + index += 1 + if not point: + continue + + self._write_d_pt_point(index, point) + + def _write_d_pt_point(self, index, point): + # Write an individual <c:dPt> element. + + self._xml_start_tag("c:dPt") + + # Write the c:idx element. + self._write_idx(index) + + # Write the c:spPr element. + self._write_sp_pr(point) + + self._xml_end_tag("c:dPt") + + def _write_d_lbls(self, labels): + # Write the <c:dLbls> element. + + if not labels: + return + + self._xml_start_tag("c:dLbls") + + # Write the custom c:dLbl elements. + if labels.get("custom"): + self._write_custom_labels(labels, labels["custom"]) + + # Write the c:numFmt element. + if labels.get("num_format"): + self._write_data_label_number_format(labels["num_format"]) + + # Write the c:spPr element for the plotarea formatting. + self._write_sp_pr(labels) + + # Write the data label font elements. + if labels.get("font"): + self._write_axis_font(labels["font"]) + + # Write the c:dLblPos element. + if labels.get("position"): + self._write_d_lbl_pos(labels["position"]) + + # Write the c:showLegendKey element. + if labels.get("legend_key"): + self._write_show_legend_key() + + # Write the c:showVal element. + if labels.get("value"): + self._write_show_val() + + # Write the c:showCatName element. + if labels.get("category"): + self._write_show_cat_name() + + # Write the c:showSerName element. + if labels.get("series_name"): + self._write_show_ser_name() + + # Write the c:showPercent element. + if labels.get("percentage"): + self._write_show_percent() + + # Write the c:separator element. + if labels.get("separator"): + self._write_separator(labels["separator"]) + + # Write the c:showLeaderLines element. + if labels.get("leader_lines"): + self._write_show_leader_lines() + + self._xml_end_tag("c:dLbls") + + def _write_custom_labels(self, parent, labels): + # Write the <c:showLegendKey> element. + index = 0 + + for label in labels: + index += 1 + + if label is None: + continue + + self._xml_start_tag("c:dLbl") + + # Write the c:idx element. + self._write_idx(index - 1) + + delete_label = label.get("delete") + + if delete_label: + self._write_delete(1) + + elif label.get("formula"): + self._write_custom_label_formula(label) + + if parent.get("position"): + self._write_d_lbl_pos(parent["position"]) + + if parent.get("value"): + self._write_show_val() + if parent.get("category"): + self._write_show_cat_name() + if parent.get("series_name"): + self._write_show_ser_name() + + elif label.get("value"): + self._write_custom_label_str(label) + + if parent.get("position"): + self._write_d_lbl_pos(parent["position"]) + + if parent.get("value"): + self._write_show_val() + if parent.get("category"): + self._write_show_cat_name() + if parent.get("series_name"): + self._write_show_ser_name() + else: + self._write_custom_label_format_only(label) + + self._xml_end_tag("c:dLbl") + + def _write_custom_label_str(self, label): + # Write parts of the <c:dLbl> element for strings. + title = label.get("value") + font = label.get("font") + has_formatting = self._has_fill_formatting(label) + + # Write the c:layout element. + self._write_layout(None, None) + + self._xml_start_tag("c:tx") + + # Write the c:rich element. + self._write_rich(title, font, False, not has_formatting) + + self._xml_end_tag("c:tx") + + # Write the c:spPr element. + self._write_sp_pr(label) + + def _write_custom_label_formula(self, label): + # Write parts of the <c:dLbl> element for formulas. + formula = label.get("formula") + data_id = label.get("data_id") + data = None + + if data_id is not None: + data = self.formula_data[data_id] + + # Write the c:layout element. + self._write_layout(None, None) + + self._xml_start_tag("c:tx") + + # Write the c:strRef element. + self._write_str_ref(formula, data, "str") + + self._xml_end_tag("c:tx") + + # Write the data label formatting, if any. + self._write_custom_label_format_only(label) + + def _write_custom_label_format_only(self, label): + # Write parts of the <c:dLbl> labels with changed formatting. + font = label.get("font") + has_formatting = self._has_fill_formatting(label) + + if has_formatting: + self._write_sp_pr(label) + self._write_tx_pr(font) + elif font: + self._xml_empty_tag("c:spPr") + self._write_tx_pr(font) + + def _write_show_legend_key(self): + # Write the <c:showLegendKey> element. + val = "1" + + attributes = [("val", val)] + + self._xml_empty_tag("c:showLegendKey", attributes) + + def _write_show_val(self): + # Write the <c:showVal> element. + val = 1 + + attributes = [("val", val)] + + self._xml_empty_tag("c:showVal", attributes) + + def _write_show_cat_name(self): + # Write the <c:showCatName> element. + val = 1 + + attributes = [("val", val)] + + self._xml_empty_tag("c:showCatName", attributes) + + def _write_show_ser_name(self): + # Write the <c:showSerName> element. + val = 1 + + attributes = [("val", val)] + + self._xml_empty_tag("c:showSerName", attributes) + + def _write_show_percent(self): + # Write the <c:showPercent> element. + val = 1 + + attributes = [("val", val)] + + self._xml_empty_tag("c:showPercent", attributes) + + def _write_separator(self, data): + # Write the <c:separator> element. + self._xml_data_element("c:separator", data) + + def _write_show_leader_lines(self): + # Write the <c:showLeaderLines> element. + # + # This is different for Pie/Doughnut charts. Other chart types only + # supported leader lines after Excel 2015 via an extension element. + # + uri = "{CE6537A1-D6FC-4f65-9D91-7224C49458BB}" + xmlns_c_15 = "http://schemas.microsoft.com/office/drawing/2012/chart" + + attributes = [ + ("uri", uri), + ("xmlns:c15", xmlns_c_15), + ] + + self._xml_start_tag("c:extLst") + self._xml_start_tag("c:ext", attributes) + self._xml_empty_tag("c15:showLeaderLines", [("val", 1)]) + self._xml_end_tag("c:ext") + self._xml_end_tag("c:extLst") + + def _write_d_lbl_pos(self, val): + # Write the <c:dLblPos> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:dLblPos", attributes) + + def _write_delete(self, val): + # Write the <c:delete> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:delete", attributes) + + def _write_c_invert_if_negative(self, invert): + # Write the <c:invertIfNegative> element. + val = 1 + + if not invert: + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:invertIfNegative", attributes) + + def _write_axis_font(self, font): + # Write the axis font elements. + + if not font: + return + + self._xml_start_tag("c:txPr") + self._write_a_body_pr(font.get("rotation"), None) + self._write_a_lst_style() + self._xml_start_tag("a:p") + + self._write_a_p_pr_rich(font) + + self._write_a_end_para_rpr() + self._xml_end_tag("a:p") + self._xml_end_tag("c:txPr") + + def _write_a_latin(self, attributes): + # Write the <a:latin> element. + self._xml_empty_tag("a:latin", attributes) + + def _write_d_table(self): + # Write the <c:dTable> element. + table = self.table + + if not table: + return + + self._xml_start_tag("c:dTable") + + if table["horizontal"]: + # Write the c:showHorzBorder element. + self._write_show_horz_border() + + if table["vertical"]: + # Write the c:showVertBorder element. + self._write_show_vert_border() + + if table["outline"]: + # Write the c:showOutline element. + self._write_show_outline() + + if table["show_keys"]: + # Write the c:showKeys element. + self._write_show_keys() + + if table["font"]: + # Write the table font. + self._write_tx_pr(table["font"]) + + self._xml_end_tag("c:dTable") + + def _write_show_horz_border(self): + # Write the <c:showHorzBorder> element. + attributes = [("val", 1)] + + self._xml_empty_tag("c:showHorzBorder", attributes) + + def _write_show_vert_border(self): + # Write the <c:showVertBorder> element. + attributes = [("val", 1)] + + self._xml_empty_tag("c:showVertBorder", attributes) + + def _write_show_outline(self): + # Write the <c:showOutline> element. + attributes = [("val", 1)] + + self._xml_empty_tag("c:showOutline", attributes) + + def _write_show_keys(self): + # Write the <c:showKeys> element. + attributes = [("val", 1)] + + self._xml_empty_tag("c:showKeys", attributes) + + def _write_error_bars(self, error_bars): + # Write the X and Y error bars. + + if not error_bars: + return + + if error_bars["x_error_bars"]: + self._write_err_bars("x", error_bars["x_error_bars"]) + + if error_bars["y_error_bars"]: + self._write_err_bars("y", error_bars["y_error_bars"]) + + def _write_err_bars(self, direction, error_bars): + # Write the <c:errBars> element. + + if not error_bars: + return + + self._xml_start_tag("c:errBars") + + # Write the c:errDir element. + self._write_err_dir(direction) + + # Write the c:errBarType element. + self._write_err_bar_type(error_bars["direction"]) + + # Write the c:errValType element. + self._write_err_val_type(error_bars["type"]) + + if not error_bars["endcap"]: + # Write the c:noEndCap element. + self._write_no_end_cap() + + if error_bars["type"] == "stdErr": + # Don't need to write a c:errValType tag. + pass + elif error_bars["type"] == "cust": + # Write the custom error tags. + self._write_custom_error(error_bars) + else: + # Write the c:val element. + self._write_error_val(error_bars["value"]) + + # Write the c:spPr element. + self._write_sp_pr(error_bars) + + self._xml_end_tag("c:errBars") + + def _write_err_dir(self, val): + # Write the <c:errDir> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:errDir", attributes) + + def _write_err_bar_type(self, val): + # Write the <c:errBarType> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:errBarType", attributes) + + def _write_err_val_type(self, val): + # Write the <c:errValType> element. + + attributes = [("val", val)] + + self._xml_empty_tag("c:errValType", attributes) + + def _write_no_end_cap(self): + # Write the <c:noEndCap> element. + attributes = [("val", 1)] + + self._xml_empty_tag("c:noEndCap", attributes) + + def _write_error_val(self, val): + # Write the <c:val> element for error bars. + + attributes = [("val", val)] + + self._xml_empty_tag("c:val", attributes) + + def _write_custom_error(self, error_bars): + # Write the custom error bars tags. + + if error_bars["plus_values"]: + # Write the c:plus element. + self._xml_start_tag("c:plus") + + if isinstance(error_bars["plus_values"], list): + self._write_num_lit(error_bars["plus_values"]) + else: + self._write_num_ref( + error_bars["plus_values"], error_bars["plus_data"], "num" + ) + self._xml_end_tag("c:plus") + + if error_bars["minus_values"]: + # Write the c:minus element. + self._xml_start_tag("c:minus") + + if isinstance(error_bars["minus_values"], list): + self._write_num_lit(error_bars["minus_values"]) + else: + self._write_num_ref( + error_bars["minus_values"], error_bars["minus_data"], "num" + ) + self._xml_end_tag("c:minus") + + def _write_num_lit(self, data): + # Write the <c:numLit> element for literal number list elements. + count = len(data) + + # Write the c:numLit element. + self._xml_start_tag("c:numLit") + + # Write the c:formatCode element. + self._write_format_code("General") + + # Write the c:ptCount element. + self._write_pt_count(count) + + for i in range(count): + token = data[i] + + if token is None: + continue + + try: + float(token) + except ValueError: + # Write non-numeric data as 0. + token = 0 + + # Write the c:pt element. + self._write_pt(i, token) + + self._xml_end_tag("c:numLit") + + def _write_up_down_bars(self): + # Write the <c:upDownBars> element. + up_down_bars = self.up_down_bars + + if up_down_bars is None: + return + + self._xml_start_tag("c:upDownBars") + + # Write the c:gapWidth element. + self._write_gap_width(150) + + # Write the c:upBars element. + self._write_up_bars(up_down_bars.get("up")) + + # Write the c:downBars element. + self._write_down_bars(up_down_bars.get("down")) + + self._xml_end_tag("c:upDownBars") + + def _write_gap_width(self, val): + # Write the <c:gapWidth> element. + + if val is None: + return + + attributes = [("val", val)] + + self._xml_empty_tag("c:gapWidth", attributes) + + def _write_up_bars(self, bar_format): + # Write the <c:upBars> element. + + if bar_format["line"] and bar_format["line"]["defined"]: + self._xml_start_tag("c:upBars") + + # Write the c:spPr element. + self._write_sp_pr(bar_format) + + self._xml_end_tag("c:upBars") + else: + self._xml_empty_tag("c:upBars") + + def _write_down_bars(self, bar_format): + # Write the <c:downBars> element. + + if bar_format["line"] and bar_format["line"]["defined"]: + self._xml_start_tag("c:downBars") + + # Write the c:spPr element. + self._write_sp_pr(bar_format) + + self._xml_end_tag("c:downBars") + else: + self._xml_empty_tag("c:downBars") + + def _write_disp_units(self, units, display): + # Write the <c:dispUnits> element. + + if not units: + return + + attributes = [("val", units)] + + self._xml_start_tag("c:dispUnits") + self._xml_empty_tag("c:builtInUnit", attributes) + + if display: + self._xml_start_tag("c:dispUnitsLbl") + self._xml_empty_tag("c:layout") + self._xml_end_tag("c:dispUnitsLbl") + + self._xml_end_tag("c:dispUnits") + + def _write_a_grad_fill(self, gradient): + # Write the <a:gradFill> element. + + attributes = [("flip", "none"), ("rotWithShape", "1")] + + if gradient["type"] == "linear": + attributes = [] + + self._xml_start_tag("a:gradFill", attributes) + + # Write the a:gsLst element. + self._write_a_gs_lst(gradient) + + if gradient["type"] == "linear": + # Write the a:lin element. + self._write_a_lin(gradient["angle"]) + else: + # Write the a:path element. + self._write_a_path(gradient["type"]) + + # Write the a:tileRect element. + self._write_a_tile_rect(gradient["type"]) + + self._xml_end_tag("a:gradFill") + + def _write_a_gs_lst(self, gradient): + # Write the <a:gsLst> element. + positions = gradient["positions"] + colors = gradient["colors"] + + self._xml_start_tag("a:gsLst") + + for i, color in enumerate(colors): + pos = int(positions[i] * 1000) + attributes = [("pos", pos)] + self._xml_start_tag("a:gs", attributes) + + # Write the a:srgbClr element. + color = _get_rgb_color(color) + self._write_a_srgb_clr(color) + + self._xml_end_tag("a:gs") + + self._xml_end_tag("a:gsLst") + + def _write_a_lin(self, angle): + # Write the <a:lin> element. + + angle = int(60000 * angle) + + attributes = [ + ("ang", angle), + ("scaled", "0"), + ] + + self._xml_empty_tag("a:lin", attributes) + + def _write_a_path(self, gradient_type): + # Write the <a:path> element. + + attributes = [("path", gradient_type)] + + self._xml_start_tag("a:path", attributes) + + # Write the a:fillToRect element. + self._write_a_fill_to_rect(gradient_type) + + self._xml_end_tag("a:path") + + def _write_a_fill_to_rect(self, gradient_type): + # Write the <a:fillToRect> element. + + if gradient_type == "shape": + attributes = [ + ("l", "50000"), + ("t", "50000"), + ("r", "50000"), + ("b", "50000"), + ] + else: + attributes = [ + ("l", "100000"), + ("t", "100000"), + ] + + self._xml_empty_tag("a:fillToRect", attributes) + + def _write_a_tile_rect(self, gradient_type): + # Write the <a:tileRect> element. + + if gradient_type == "shape": + attributes = [] + else: + attributes = [ + ("r", "-100000"), + ("b", "-100000"), + ] + + self._xml_empty_tag("a:tileRect", attributes) + + def _write_a_patt_fill(self, pattern): + # Write the <a:pattFill> element. + + attributes = [("prst", pattern["pattern"])] + + self._xml_start_tag("a:pattFill", attributes) + + # Write the a:fgClr element. + self._write_a_fg_clr(pattern["fg_color"]) + + # Write the a:bgClr element. + self._write_a_bg_clr(pattern["bg_color"]) + + self._xml_end_tag("a:pattFill") + + def _write_a_fg_clr(self, color): + # Write the <a:fgClr> element. + + color = _get_rgb_color(color) + + self._xml_start_tag("a:fgClr") + + # Write the a:srgbClr element. + self._write_a_srgb_clr(color) + + self._xml_end_tag("a:fgClr") + + def _write_a_bg_clr(self, color): + # Write the <a:bgClr> element. + + color = _get_rgb_color(color) + + self._xml_start_tag("a:bgClr") + + # Write the a:srgbClr element. + self._write_a_srgb_clr(color) + + self._xml_end_tag("a:bgClr") diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/chart_area.py b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_area.py new file mode 100644 index 00000000..4747e5b0 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_area.py @@ -0,0 +1,102 @@ +############################################################################### +# +# ChartArea - A class for writing the Excel XLSX Area charts. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from . import chart + + +class ChartArea(chart.Chart): + """ + A class for writing the Excel XLSX Area charts. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self, options=None): + """ + Constructor. + + """ + super().__init__() + + if options is None: + options = {} + + self.subtype = options.get("subtype") + + if not self.subtype: + self.subtype = "standard" + + self.cross_between = "midCat" + self.show_crosses = False + + # Override and reset the default axis values. + if self.subtype == "percent_stacked": + self.y_axis["defaults"]["num_format"] = "0%" + + # Set the available data label positions for this chart type. + self.label_position_default = "center" + self.label_positions = {"center": "ctr"} + + self.set_y_axis({}) + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _write_chart_type(self, args): + # Override the virtual superclass method with a chart specific method. + # Write the c:areaChart element. + self._write_area_chart(args) + + ########################################################################### + # + # XML methods. + # + ########################################################################### + # + def _write_area_chart(self, args): + # Write the <c:areaChart> element. + + if args["primary_axes"]: + series = self._get_primary_axes_series() + else: + series = self._get_secondary_axes_series() + + if not series: + return + + subtype = self.subtype + + if subtype == "percent_stacked": + subtype = "percentStacked" + + self._xml_start_tag("c:areaChart") + + # Write the c:grouping element. + self._write_grouping(subtype) + + # Write the series elements. + for data in series: + self._write_ser(data) + + # Write the c:dropLines element. + self._write_drop_lines() + + # Write the c:axId elements + self._write_axis_ids(args) + + self._xml_end_tag("c:areaChart") diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/chart_bar.py b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_bar.py new file mode 100644 index 00000000..cd138083 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_bar.py @@ -0,0 +1,176 @@ +############################################################################### +# +# ChartBar - A class for writing the Excel XLSX Bar charts. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from warnings import warn + +from . import chart + + +class ChartBar(chart.Chart): + """ + A class for writing the Excel XLSX Bar charts. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self, options=None): + """ + Constructor. + + """ + super().__init__() + + if options is None: + options = {} + + self.subtype = options.get("subtype") + + if not self.subtype: + self.subtype = "clustered" + + self.cat_axis_position = "l" + self.val_axis_position = "b" + self.horiz_val_axis = 0 + self.horiz_cat_axis = 1 + self.show_crosses = False + + # Override and reset the default axis values. + self.x_axis["defaults"]["major_gridlines"] = {"visible": 1} + self.y_axis["defaults"]["major_gridlines"] = {"visible": 0} + + if self.subtype == "percent_stacked": + self.x_axis["defaults"]["num_format"] = "0%" + + # Set the available data label positions for this chart type. + self.label_position_default = "outside_end" + self.label_positions = { + "center": "ctr", + "inside_base": "inBase", + "inside_end": "inEnd", + "outside_end": "outEnd", + } + + self.set_x_axis({}) + self.set_y_axis({}) + + def combine(self, chart=None): + # pylint: disable=redefined-outer-name + """ + Create a combination chart with a secondary chart. + + Note: Override parent method to add an extra check that is required + for Bar charts to ensure that their combined chart is on a secondary + axis. + + Args: + chart: The secondary chart to combine with the primary chart. + + Returns: + Nothing. + + """ + if chart is None: + return + + if not chart.is_secondary: + warn("Charts combined with Bar charts must be on a secondary axis") + + self.combined = chart + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _write_chart_type(self, args): + # Override the virtual superclass method with a chart specific method. + if args["primary_axes"]: + # Reverse X and Y axes for Bar charts. + tmp = self.y_axis + self.y_axis = self.x_axis + self.x_axis = tmp + + if self.y2_axis["position"] == "r": + self.y2_axis["position"] = "t" + + # Write the c:barChart element. + self._write_bar_chart(args) + + def _write_bar_chart(self, args): + # Write the <c:barChart> element. + + if args["primary_axes"]: + series = self._get_primary_axes_series() + else: + series = self._get_secondary_axes_series() + + if not series: + return + + subtype = self.subtype + if subtype == "percent_stacked": + subtype = "percentStacked" + + # Set a default overlap for stacked charts. + if "stacked" in self.subtype and self.series_overlap_1 is None: + self.series_overlap_1 = 100 + + self._xml_start_tag("c:barChart") + + # Write the c:barDir element. + self._write_bar_dir() + + # Write the c:grouping element. + self._write_grouping(subtype) + + # Write the c:ser elements. + for data in series: + self._write_ser(data) + + # Write the c:gapWidth element. + if args["primary_axes"]: + self._write_gap_width(self.series_gap_1) + else: + self._write_gap_width(self.series_gap_2) + + # Write the c:overlap element. + if args["primary_axes"]: + self._write_overlap(self.series_overlap_1) + else: + self._write_overlap(self.series_overlap_2) + + # Write the c:axId elements + self._write_axis_ids(args) + + self._xml_end_tag("c:barChart") + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_bar_dir(self): + # Write the <c:barDir> element. + val = "bar" + + attributes = [("val", val)] + + self._xml_empty_tag("c:barDir", attributes) + + def _write_err_dir(self, val): + # Overridden from Chart class since it is not used in Bar charts. + pass diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/chart_column.py b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_column.py new file mode 100644 index 00000000..6211b3b5 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_column.py @@ -0,0 +1,133 @@ +############################################################################### +# +# ChartColumn - A class for writing the Excel XLSX Column charts. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from . import chart + + +class ChartColumn(chart.Chart): + """ + A class for writing the Excel XLSX Column charts. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self, options=None): + """ + Constructor. + + """ + super().__init__() + + if options is None: + options = {} + + self.subtype = options.get("subtype") + + if not self.subtype: + self.subtype = "clustered" + + self.horiz_val_axis = 0 + + if self.subtype == "percent_stacked": + self.y_axis["defaults"]["num_format"] = "0%" + + # Set the available data label positions for this chart type. + self.label_position_default = "outside_end" + self.label_positions = { + "center": "ctr", + "inside_base": "inBase", + "inside_end": "inEnd", + "outside_end": "outEnd", + } + + self.set_y_axis({}) + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _write_chart_type(self, args): + # Override the virtual superclass method with a chart specific method. + + # Write the c:barChart element. + self._write_bar_chart(args) + + def _write_bar_chart(self, args): + # Write the <c:barChart> element. + + if args["primary_axes"]: + series = self._get_primary_axes_series() + else: + series = self._get_secondary_axes_series() + + if not series: + return + + subtype = self.subtype + if subtype == "percent_stacked": + subtype = "percentStacked" + + # Set a default overlap for stacked charts. + if "stacked" in self.subtype and self.series_overlap_1 is None: + self.series_overlap_1 = 100 + + self._xml_start_tag("c:barChart") + + # Write the c:barDir element. + self._write_bar_dir() + + # Write the c:grouping element. + self._write_grouping(subtype) + + # Write the c:ser elements. + for data in series: + self._write_ser(data) + + # Write the c:gapWidth element. + if args["primary_axes"]: + self._write_gap_width(self.series_gap_1) + else: + self._write_gap_width(self.series_gap_2) + + # Write the c:overlap element. + if args["primary_axes"]: + self._write_overlap(self.series_overlap_1) + else: + self._write_overlap(self.series_overlap_2) + + # Write the c:axId elements + self._write_axis_ids(args) + + self._xml_end_tag("c:barChart") + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_bar_dir(self): + # Write the <c:barDir> element. + val = "col" + + attributes = [("val", val)] + + self._xml_empty_tag("c:barDir", attributes) + + def _write_err_dir(self, val): + # Overridden from Chart class since it is not used in Column charts. + pass diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/chart_doughnut.py b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_doughnut.py new file mode 100644 index 00000000..d4cb4f4f --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_doughnut.py @@ -0,0 +1,101 @@ +############################################################################### +# +# ChartDoughnut - A class for writing the Excel XLSX Doughnut charts. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from warnings import warn + +from . import chart_pie + + +class ChartDoughnut(chart_pie.ChartPie): + """ + A class for writing the Excel XLSX Doughnut charts. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + super().__init__() + + self.vary_data_color = 1 + self.rotation = 0 + self.hole_size = 50 + + def set_hole_size(self, size): + """ + Set the Doughnut chart hole size. + + Args: + size: 10 <= size <= 90. + + Returns: + Nothing. + + """ + if size is None: + return + + # Ensure the size is in Excel's range. + if size < 10 or size > 90: + warn("Chart hole size '{size}' outside Excel range: 10 <= size <= 90") + return + + self.hole_size = int(size) + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _write_chart_type(self, args): + # Override the virtual superclass method with a chart specific method. + # Write the c:doughnutChart element. + self._write_doughnut_chart() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_doughnut_chart(self): + # Write the <c:doughnutChart> element. Over-ridden method to remove + # axis_id code since Doughnut charts don't require val and cat axes. + self._xml_start_tag("c:doughnutChart") + + # Write the c:varyColors element. + self._write_vary_colors() + + # Write the series elements. + for data in self.series: + self._write_ser(data) + + # Write the c:firstSliceAng element. + self._write_first_slice_ang() + + # Write the c:holeSize element. + self._write_c_hole_size() + + self._xml_end_tag("c:doughnutChart") + + def _write_c_hole_size(self): + # Write the <c:holeSize> element. + attributes = [("val", self.hole_size)] + + self._xml_empty_tag("c:holeSize", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/chart_line.py b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_line.py new file mode 100644 index 00000000..949a94de --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_line.py @@ -0,0 +1,144 @@ +############################################################################### +# +# ChartLine - A class for writing the Excel XLSX Line charts. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from . import chart + + +class ChartLine(chart.Chart): + """ + A class for writing the Excel XLSX Line charts. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self, options=None): + """ + Constructor. + + """ + super().__init__() + + if options is None: + options = {} + + self.subtype = options.get("subtype") + + if not self.subtype: + self.subtype = "standard" + + self.default_marker = {"type": "none"} + self.smooth_allowed = True + + # Override and reset the default axis values. + if self.subtype == "percent_stacked": + self.y_axis["defaults"]["num_format"] = "0%" + + # Set the available data label positions for this chart type. + self.label_position_default = "right" + self.label_positions = { + "center": "ctr", + "right": "r", + "left": "l", + "above": "t", + "below": "b", + # For backward compatibility. + "top": "t", + "bottom": "b", + } + + self.set_y_axis({}) + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _write_chart_type(self, args): + # Override the virtual superclass method with a chart specific method. + # Write the c:lineChart element. + self._write_line_chart(args) + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_line_chart(self, args): + # Write the <c:lineChart> element. + + if args["primary_axes"]: + series = self._get_primary_axes_series() + else: + series = self._get_secondary_axes_series() + + if not series: + return + + subtype = self.subtype + + if subtype == "percent_stacked": + subtype = "percentStacked" + + self._xml_start_tag("c:lineChart") + + # Write the c:grouping element. + self._write_grouping(subtype) + + # Write the series elements. + for data in series: + self._write_ser(data) + + # Write the c:dropLines element. + self._write_drop_lines() + + # Write the c:hiLowLines element. + self._write_hi_low_lines() + + # Write the c:upDownBars element. + self._write_up_down_bars() + + # Write the c:marker element. + self._write_marker_value() + + # Write the c:axId elements + self._write_axis_ids(args) + + self._xml_end_tag("c:lineChart") + + def _write_d_pt_point(self, index, point): + # Write an individual <c:dPt> element. Override the parent method to + # add markers. + + self._xml_start_tag("c:dPt") + + # Write the c:idx element. + self._write_idx(index) + + self._xml_start_tag("c:marker") + + # Write the c:spPr element. + self._write_sp_pr(point) + + self._xml_end_tag("c:marker") + + self._xml_end_tag("c:dPt") + + def _write_marker_value(self): + # Write the <c:marker> element without a sub-element. + attributes = [("val", 1)] + + self._xml_empty_tag("c:marker", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/chart_pie.py b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_pie.py new file mode 100644 index 00000000..2b55ba76 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_pie.py @@ -0,0 +1,263 @@ +############################################################################### +# +# ChartPie - A class for writing the Excel XLSX Pie charts. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from warnings import warn + +from . import chart + + +class ChartPie(chart.Chart): + """ + A class for writing the Excel XLSX Pie charts. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + super().__init__() + + self.vary_data_color = 1 + self.rotation = 0 + + # Set the available data label positions for this chart type. + self.label_position_default = "best_fit" + self.label_positions = { + "center": "ctr", + "inside_end": "inEnd", + "outside_end": "outEnd", + "best_fit": "bestFit", + } + + def set_rotation(self, rotation): + """ + Set the Pie/Doughnut chart rotation: the angle of the first slice. + + Args: + rotation: First segment angle: 0 <= rotation <= 360. + + Returns: + Nothing. + + """ + if rotation is None: + return + + # Ensure the rotation is in Excel's range. + if rotation < 0 or rotation > 360: + warn( + f"Chart rotation '{rotation}' outside Excel range: 0 <= rotation <= 360" + ) + return + + self.rotation = int(rotation) + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _write_chart_type(self, args): + # Override the virtual superclass method with a chart specific method. + # Write the c:pieChart element. + self._write_pie_chart() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_pie_chart(self): + # Write the <c:pieChart> element. Over-ridden method to remove + # axis_id code since Pie charts don't require val and cat axes. + self._xml_start_tag("c:pieChart") + + # Write the c:varyColors element. + self._write_vary_colors() + + # Write the series elements. + for data in self.series: + self._write_ser(data) + + # Write the c:firstSliceAng element. + self._write_first_slice_ang() + + self._xml_end_tag("c:pieChart") + + def _write_plot_area(self): + # Over-ridden method to remove the cat_axis() and val_axis() code + # since Pie charts don't require those axes. + # + # Write the <c:plotArea> element. + + self._xml_start_tag("c:plotArea") + + # Write the c:layout element. + self._write_layout(self.plotarea.get("layout"), "plot") + + # Write the subclass chart type element. + self._write_chart_type(None) + # Configure a combined chart if present. + second_chart = self.combined + + if second_chart: + # Secondary axis has unique id otherwise use same as primary. + if second_chart.is_secondary: + second_chart.id = 1000 + self.id + else: + second_chart.id = self.id + + # Share the same filehandle for writing. + second_chart.fh = self.fh + + # Share series index with primary chart. + second_chart.series_index = self.series_index + + # Write the subclass chart type elements for combined chart. + # pylint: disable-next=protected-access + second_chart._write_chart_type(None) + + # Write the c:spPr element for the plotarea formatting. + self._write_sp_pr(self.plotarea) + + self._xml_end_tag("c:plotArea") + + def _write_legend(self): + # Over-ridden method to add <c:txPr> to legend. + # Write the <c:legend> element. + legend = self.legend + position = legend.get("position", "right") + font = legend.get("font") + delete_series = [] + overlay = 0 + + if legend.get("delete_series") and isinstance(legend["delete_series"], list): + delete_series = legend["delete_series"] + + if position.startswith("overlay_"): + position = position.replace("overlay_", "") + overlay = 1 + + allowed = { + "right": "r", + "left": "l", + "top": "t", + "bottom": "b", + "top_right": "tr", + } + + if position == "none": + return + + if position not in allowed: + return + + position = allowed[position] + + self._xml_start_tag("c:legend") + + # Write the c:legendPos element. + self._write_legend_pos(position) + + # Remove series labels from the legend. + for index in delete_series: + # Write the c:legendEntry element. + self._write_legend_entry(index) + + # Write the c:layout element. + self._write_layout(legend.get("layout"), "legend") + + # Write the c:overlay element. + if overlay: + self._write_overlay() + + # Write the c:spPr element. + self._write_sp_pr(legend) + + # Write the c:txPr element. Over-ridden. + self._write_tx_pr_legend(None, font) + + self._xml_end_tag("c:legend") + + def _write_tx_pr_legend(self, horiz, font): + # Write the <c:txPr> element for legends. + + if font and font.get("rotation"): + rotation = font["rotation"] + else: + rotation = None + + self._xml_start_tag("c:txPr") + + # Write the a:bodyPr element. + self._write_a_body_pr(rotation, horiz) + + # Write the a:lstStyle element. + self._write_a_lst_style() + + # Write the a:p element. + self._write_a_p_legend(font) + + self._xml_end_tag("c:txPr") + + def _write_a_p_legend(self, font): + # Write the <a:p> element for legends. + + self._xml_start_tag("a:p") + + # Write the a:pPr element. + self._write_a_p_pr_legend(font) + + # Write the a:endParaRPr element. + self._write_a_end_para_rpr() + + self._xml_end_tag("a:p") + + def _write_a_p_pr_legend(self, font): + # Write the <a:pPr> element for legends. + attributes = [("rtl", 0)] + + self._xml_start_tag("a:pPr", attributes) + + # Write the a:defRPr element. + self._write_a_def_rpr(font) + + self._xml_end_tag("a:pPr") + + def _write_vary_colors(self): + # Write the <c:varyColors> element. + attributes = [("val", 1)] + + self._xml_empty_tag("c:varyColors", attributes) + + def _write_first_slice_ang(self): + # Write the <c:firstSliceAng> element. + attributes = [("val", self.rotation)] + + self._xml_empty_tag("c:firstSliceAng", attributes) + + def _write_show_leader_lines(self): + # Write the <c:showLeaderLines> element. + # + # This is for Pie/Doughnut charts. Other chart types only supported + # leader lines after Excel 2015 via an extension element. + attributes = [("val", 1)] + + self._xml_empty_tag("c:showLeaderLines", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/chart_radar.py b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_radar.py new file mode 100644 index 00000000..6b0c8b47 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_radar.py @@ -0,0 +1,103 @@ +############################################################################### +# +# ChartRadar - A class for writing the Excel XLSX Radar charts. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from . import chart + + +class ChartRadar(chart.Chart): + """ + A class for writing the Excel XLSX Radar charts. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self, options=None): + """ + Constructor. + + """ + super().__init__() + + if options is None: + options = {} + + self.subtype = options.get("subtype") + + if not self.subtype: + self.subtype = "marker" + self.default_marker = {"type": "none"} + + # Override and reset the default axis values. + self.x_axis["defaults"]["major_gridlines"] = {"visible": 1} + self.set_x_axis({}) + + # Set the available data label positions for this chart type. + self.label_position_default = "center" + self.label_positions = {"center": "ctr"} + + # Hardcode major_tick_mark for now until there is an accessor. + self.y_axis["major_tick_mark"] = "cross" + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _write_chart_type(self, args): + # Write the c:radarChart element. + self._write_radar_chart(args) + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_radar_chart(self, args): + # Write the <c:radarChart> element. + + if args["primary_axes"]: + series = self._get_primary_axes_series() + else: + series = self._get_secondary_axes_series() + + if not series: + return + + self._xml_start_tag("c:radarChart") + + # Write the c:radarStyle element. + self._write_radar_style() + + # Write the series elements. + for data in series: + self._write_ser(data) + + # Write the c:axId elements + self._write_axis_ids(args) + + self._xml_end_tag("c:radarChart") + + def _write_radar_style(self): + # Write the <c:radarStyle> element. + val = "marker" + + if self.subtype == "filled": + val = "filled" + + attributes = [("val", val)] + + self._xml_empty_tag("c:radarStyle", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/chart_scatter.py b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_scatter.py new file mode 100644 index 00000000..322eb9a0 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_scatter.py @@ -0,0 +1,336 @@ +############################################################################### +# +# ChartScatter - A class for writing the Excel XLSX Scatter charts. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from warnings import warn + +from . import chart + + +class ChartScatter(chart.Chart): + """ + A class for writing the Excel XLSX Scatter charts. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self, options=None): + """ + Constructor. + + """ + super().__init__() + + if options is None: + options = {} + + self.subtype = options.get("subtype") + + if not self.subtype: + self.subtype = "marker_only" + + self.cross_between = "midCat" + self.horiz_val_axis = 0 + self.val_axis_position = "b" + self.smooth_allowed = True + self.requires_category = True + + # Set the available data label positions for this chart type. + self.label_position_default = "right" + self.label_positions = { + "center": "ctr", + "right": "r", + "left": "l", + "above": "t", + "below": "b", + # For backward compatibility. + "top": "t", + "bottom": "b", + } + + def combine(self, chart=None): + # pylint: disable=redefined-outer-name + """ + Create a combination chart with a secondary chart. + + Note: Override parent method to add a warning. + + Args: + chart: The secondary chart to combine with the primary chart. + + Returns: + Nothing. + + """ + if chart is None: + return + + warn( + "Combined chart not currently supported with scatter chart " + "as the primary chart" + ) + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _write_chart_type(self, args): + # Override the virtual superclass method with a chart specific method. + # Write the c:scatterChart element. + self._write_scatter_chart(args) + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_scatter_chart(self, args): + # Write the <c:scatterChart> element. + + if args["primary_axes"]: + series = self._get_primary_axes_series() + else: + series = self._get_secondary_axes_series() + + if not series: + return + + style = "lineMarker" + subtype = self.subtype + + # Set the user defined chart subtype. + if subtype == "marker_only": + style = "lineMarker" + + if subtype == "straight_with_markers": + style = "lineMarker" + + if subtype == "straight": + style = "lineMarker" + self.default_marker = {"type": "none"} + + if subtype == "smooth_with_markers": + style = "smoothMarker" + + if subtype == "smooth": + style = "smoothMarker" + self.default_marker = {"type": "none"} + + # Add default formatting to the series data. + self._modify_series_formatting() + + self._xml_start_tag("c:scatterChart") + + # Write the c:scatterStyle element. + self._write_scatter_style(style) + + # Write the series elements. + for data in series: + self._write_ser(data) + + # Write the c:axId elements + self._write_axis_ids(args) + + self._xml_end_tag("c:scatterChart") + + def _write_ser(self, series): + # Over-ridden to write c:xVal/c:yVal instead of c:cat/c:val elements. + # Write the <c:ser> element. + + index = self.series_index + self.series_index += 1 + + self._xml_start_tag("c:ser") + + # Write the c:idx element. + self._write_idx(index) + + # Write the c:order element. + self._write_order(index) + + # Write the series name. + self._write_series_name(series) + + # Write the c:spPr element. + self._write_sp_pr(series) + + # Write the c:marker element. + self._write_marker(series.get("marker")) + + # Write the c:dPt element. + self._write_d_pt(series.get("points")) + + # Write the c:dLbls element. + self._write_d_lbls(series.get("labels")) + + # Write the c:trendline element. + self._write_trendline(series.get("trendline")) + + # Write the c:errBars element. + self._write_error_bars(series.get("error_bars")) + + # Write the c:xVal element. + self._write_x_val(series) + + # Write the c:yVal element. + self._write_y_val(series) + + # Write the c:smooth element. + if "smooth" in self.subtype and series["smooth"] is None: + # Default is on for smooth scatter charts. + self._write_c_smooth(True) + else: + self._write_c_smooth(series["smooth"]) + + self._xml_end_tag("c:ser") + + def _write_plot_area(self): + # Over-ridden to have 2 valAx elements for scatter charts instead + # of catAx/valAx. + # + # Write the <c:plotArea> element. + self._xml_start_tag("c:plotArea") + + # Write the c:layout element. + self._write_layout(self.plotarea.get("layout"), "plot") + + # Write the subclass chart elements for primary and secondary axes. + self._write_chart_type({"primary_axes": 1}) + self._write_chart_type({"primary_axes": 0}) + + # Write c:catAx and c:valAx elements for series using primary axes. + self._write_cat_val_axis( + { + "x_axis": self.x_axis, + "y_axis": self.y_axis, + "axis_ids": self.axis_ids, + "position": "b", + } + ) + + tmp = self.horiz_val_axis + self.horiz_val_axis = 1 + + self._write_val_axis( + { + "x_axis": self.x_axis, + "y_axis": self.y_axis, + "axis_ids": self.axis_ids, + "position": "l", + } + ) + + self.horiz_val_axis = tmp + + # Write c:valAx and c:catAx elements for series using secondary axes + self._write_cat_val_axis( + { + "x_axis": self.x2_axis, + "y_axis": self.y2_axis, + "axis_ids": self.axis2_ids, + "position": "b", + } + ) + self.horiz_val_axis = 1 + self._write_val_axis( + { + "x_axis": self.x2_axis, + "y_axis": self.y2_axis, + "axis_ids": self.axis2_ids, + "position": "l", + } + ) + + # Write the c:spPr element for the plotarea formatting. + self._write_sp_pr(self.plotarea) + + self._xml_end_tag("c:plotArea") + + def _write_x_val(self, series): + # Write the <c:xVal> element. + formula = series.get("categories") + data_id = series.get("cat_data_id") + data = self.formula_data[data_id] + + self._xml_start_tag("c:xVal") + + # Check the type of cached data. + data_type = self._get_data_type(data) + + if data_type == "str": + # Write the c:numRef element. + self._write_str_ref(formula, data, data_type) + else: + # Write the c:numRef element. + self._write_num_ref(formula, data, data_type) + + self._xml_end_tag("c:xVal") + + def _write_y_val(self, series): + # Write the <c:yVal> element. + formula = series.get("values") + data_id = series.get("val_data_id") + data = self.formula_data[data_id] + + self._xml_start_tag("c:yVal") + + # Unlike Cat axes data should only be numeric. + # Write the c:numRef element. + self._write_num_ref(formula, data, "num") + + self._xml_end_tag("c:yVal") + + def _write_scatter_style(self, val): + # Write the <c:scatterStyle> element. + attributes = [("val", val)] + + self._xml_empty_tag("c:scatterStyle", attributes) + + def _modify_series_formatting(self): + # Add default formatting to the series data unless it has already been + # specified by the user. + subtype = self.subtype + + # The default scatter style "markers only" requires a line type. + if subtype == "marker_only": + # Go through each series and define default values. + for series in self.series: + # Set a line type unless there is already a user defined type. + if not series["line"]["defined"]: + series["line"] = { + "width": 2.25, + "none": 1, + "defined": 1, + } + + def _write_d_pt_point(self, index, point): + # Write an individual <c:dPt> element. Override the parent method to + # add markers. + + self._xml_start_tag("c:dPt") + + # Write the c:idx element. + self._write_idx(index) + + self._xml_start_tag("c:marker") + + # Write the c:spPr element. + self._write_sp_pr(point) + + self._xml_end_tag("c:marker") + + self._xml_end_tag("c:dPt") diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/chart_stock.py b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_stock.py new file mode 100644 index 00000000..640f3565 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/chart_stock.py @@ -0,0 +1,125 @@ +############################################################################### +# +# ChartStock - A class for writing the Excel XLSX Stock charts. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from . import chart + + +class ChartStock(chart.Chart): + """ + A class for writing the Excel XLSX Stock charts. + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + super().__init__() + + self.show_crosses = False + self.hi_low_lines = {} + self.date_category = True + + # Override and reset the default axis values. + self.x_axis["defaults"]["num_format"] = "dd/mm/yyyy" + self.x2_axis["defaults"]["num_format"] = "dd/mm/yyyy" + + # Set the available data label positions for this chart type. + self.label_position_default = "right" + self.label_positions = { + "center": "ctr", + "right": "r", + "left": "l", + "above": "t", + "below": "b", + # For backward compatibility. + "top": "t", + "bottom": "b", + } + + self.set_x_axis({}) + self.set_x2_axis({}) + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _write_chart_type(self, args): + # Override the virtual superclass method with a chart specific method. + # Write the c:stockChart element. + self._write_stock_chart(args) + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_stock_chart(self, args): + # Write the <c:stockChart> element. + # Overridden to add hi_low_lines(). + + if args["primary_axes"]: + series = self._get_primary_axes_series() + else: + series = self._get_secondary_axes_series() + + if not series: + return + + # Add default formatting to the series data. + self._modify_series_formatting() + + self._xml_start_tag("c:stockChart") + + # Write the series elements. + for data in series: + self._write_ser(data) + + # Write the c:dropLines element. + self._write_drop_lines() + + # Write the c:hiLowLines element. + if args.get("primary_axes"): + self._write_hi_low_lines() + + # Write the c:upDownBars element. + self._write_up_down_bars() + + # Write the c:axId elements + self._write_axis_ids(args) + + self._xml_end_tag("c:stockChart") + + def _modify_series_formatting(self): + # Add default formatting to the series data. + + index = 0 + + for series in self.series: + if index % 4 != 3: + if not series["line"]["defined"]: + series["line"] = {"width": 2.25, "none": 1, "defined": 1} + + if series["marker"] is None: + if index % 4 == 2: + series["marker"] = {"type": "dot", "size": 3} + else: + series["marker"] = {"type": "none"} + + index += 1 diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/chartsheet.py b/.venv/lib/python3.12/site-packages/xlsxwriter/chartsheet.py new file mode 100644 index 00000000..bfce373b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/chartsheet.py @@ -0,0 +1,197 @@ +############################################################################### +# +# Chartsheet - A class for writing the Excel XLSX Worksheet file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from . import worksheet +from .drawing import Drawing + + +class Chartsheet(worksheet.Worksheet): + """ + A class for writing the Excel XLSX Chartsheet file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + self.is_chartsheet = True + self.drawing = None + self.chart = None + self.charts = [] + self.zoom_scale_normal = 0 + self.orientation = 0 + self.protection = False + + def set_chart(self, chart): + """ + Set the chart object for the chartsheet. + Args: + chart: Chart object. + Returns: + chart: A reference to the chart object. + """ + chart.embedded = False + chart.protection = self.protection + self.chart = chart + self.charts.append([0, 0, chart, 0, 0, 1, 1]) + return chart + + def protect(self, password="", options=None): + """ + Set the password and protection options of the worksheet. + + Args: + password: An optional password string. + options: A dictionary of worksheet objects to protect. + + Returns: + Nothing. + + """ + # This method is overridden from parent worksheet class. + + # Chartsheets only allow a reduced set of protect options. + copy = {} + + if not options: + options = {} + + if options.get("objects") is None: + copy["objects"] = False + else: + # Objects are default on for chartsheets, so reverse state. + copy["objects"] = not options["objects"] + + if options.get("content") is None: + copy["content"] = True + else: + copy["content"] = options["content"] + + copy["sheet"] = False + copy["scenarios"] = True + + # If objects and content are both off then the chartsheet isn't + # protected, unless it has a password. + if password == "" and copy["objects"] and not copy["content"]: + return + + if self.chart: + self.chart.protection = True + else: + self.protection = True + + # Call the parent method. + super().protect(password, copy) + + ########################################################################### + # + # Private API. + # + ########################################################################### + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + # Write the root worksheet element. + self._write_chartsheet() + + # Write the worksheet properties. + self._write_sheet_pr() + + # Write the sheet view properties. + self._write_sheet_views() + + # Write the sheetProtection element. + self._write_sheet_protection() + + # Write the printOptions element. + self._write_print_options() + + # Write the worksheet page_margins. + self._write_page_margins() + + # Write the worksheet page setup. + self._write_page_setup() + + # Write the headerFooter element. + self._write_header_footer() + + # Write the drawing element. + self._write_drawings() + + # Write the legacyDrawingHF element. + self._write_legacy_drawing_hf() + + # Close the worksheet tag. + self._xml_end_tag("chartsheet") + + # Close the file. + self._xml_close() + + def _prepare_chart(self, index, chart_id, drawing_id): + # Set up chart/drawings. + + self.chart.id = chart_id - 1 + + self.drawing = Drawing() + self.drawing.orientation = self.orientation + + self.external_drawing_links.append( + ["/drawing", "../drawings/drawing" + str(drawing_id) + ".xml"] + ) + + self.drawing_links.append( + ["/chart", "../charts/chart" + str(chart_id) + ".xml"] + ) + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_chartsheet(self): + # Write the <worksheet> element. This is the root element. + + schema = "http://schemas.openxmlformats.org/" + xmlns = schema + "spreadsheetml/2006/main" + xmlns_r = schema + "officeDocument/2006/relationships" + + attributes = [("xmlns", xmlns), ("xmlns:r", xmlns_r)] + + self._xml_start_tag("chartsheet", attributes) + + def _write_sheet_pr(self): + # Write the <sheetPr> element for Sheet level properties. + attributes = [] + + if self.filter_on: + attributes.append(("filterMode", 1)) + + if self.fit_page or self.tab_color: + self._xml_start_tag("sheetPr", attributes) + self._write_tab_color() + self._write_page_set_up_pr() + self._xml_end_tag("sheetPr") + else: + self._xml_empty_tag("sheetPr", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/comments.py b/.venv/lib/python3.12/site-packages/xlsxwriter/comments.py new file mode 100644 index 00000000..06fa66e7 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/comments.py @@ -0,0 +1,212 @@ +############################################################################### +# +# Comments - A class for writing the Excel XLSX Worksheet file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from . import xmlwriter +from .utility import _preserve_whitespace, xl_rowcol_to_cell + + +class Comments(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX Comments file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + self.author_ids = {} + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self, comments_data=None): + # Assemble and write the XML file. + + if comments_data is None: + comments_data = [] + + # Write the XML declaration. + self._xml_declaration() + + # Write the comments element. + self._write_comments() + + # Write the authors element. + self._write_authors(comments_data) + + # Write the commentList element. + self._write_comment_list(comments_data) + + self._xml_end_tag("comments") + + # Close the file. + self._xml_close() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_comments(self): + # Write the <comments> element. + xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" + + attributes = [("xmlns", xmlns)] + + self._xml_start_tag("comments", attributes) + + def _write_authors(self, comment_data): + # Write the <authors> element. + author_count = 0 + + self._xml_start_tag("authors") + + for comment in comment_data: + author = comment[3] + + if author is not None and author not in self.author_ids: + # Store the author id. + self.author_ids[author] = author_count + author_count += 1 + + # Write the author element. + self._write_author(author) + + self._xml_end_tag("authors") + + def _write_author(self, data): + # Write the <author> element. + self._xml_data_element("author", data) + + def _write_comment_list(self, comment_data): + # Write the <commentList> element. + self._xml_start_tag("commentList") + + for comment in comment_data: + row = comment[0] + col = comment[1] + text = comment[2] + author = comment[3] + font_name = comment[6] + font_size = comment[7] + font_family = comment[8] + + # Look up the author id. + author_id = None + if author is not None: + author_id = self.author_ids[author] + + # Write the comment element. + font = (font_name, font_size, font_family) + self._write_comment(row, col, text, author_id, font) + + self._xml_end_tag("commentList") + + def _write_comment(self, row, col, text, author_id, font): + # Write the <comment> element. + ref = xl_rowcol_to_cell(row, col) + + attributes = [("ref", ref)] + + if author_id is not None: + attributes.append(("authorId", author_id)) + + self._xml_start_tag("comment", attributes) + + # Write the text element. + self._write_text(text, font) + + self._xml_end_tag("comment") + + def _write_text(self, text, font): + # Write the <text> element. + self._xml_start_tag("text") + + # Write the text r element. + self._write_text_r(text, font) + + self._xml_end_tag("text") + + def _write_text_r(self, text, font): + # Write the <r> element. + self._xml_start_tag("r") + + # Write the rPr element. + self._write_r_pr(font) + + # Write the text r element. + self._write_text_t(text) + + self._xml_end_tag("r") + + def _write_text_t(self, text): + # Write the text <t> element. + attributes = [] + + if _preserve_whitespace(text): + attributes.append(("xml:space", "preserve")) + + self._xml_data_element("t", text, attributes) + + def _write_r_pr(self, font): + # Write the <rPr> element. + self._xml_start_tag("rPr") + + # Write the sz element. + self._write_sz(font[1]) + + # Write the color element. + self._write_color() + + # Write the rFont element. + self._write_r_font(font[0]) + + # Write the family element. + self._write_family(font[2]) + + self._xml_end_tag("rPr") + + def _write_sz(self, font_size): + # Write the <sz> element. + attributes = [("val", font_size)] + + self._xml_empty_tag("sz", attributes) + + def _write_color(self): + # Write the <color> element. + attributes = [("indexed", 81)] + + self._xml_empty_tag("color", attributes) + + def _write_r_font(self, font_name): + # Write the <rFont> element. + attributes = [("val", font_name)] + + self._xml_empty_tag("rFont", attributes) + + def _write_family(self, font_family): + # Write the <family> element. + attributes = [("val", font_family)] + + self._xml_empty_tag("family", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/contenttypes.py b/.venv/lib/python3.12/site-packages/xlsxwriter/contenttypes.py new file mode 100644 index 00000000..bc144406 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/contenttypes.py @@ -0,0 +1,269 @@ +############################################################################### +# +# ContentTypes - A class for writing the Excel XLSX ContentTypes file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +import copy + +from . import xmlwriter + +# Long namespace strings used in the class. +APP_PACKAGE = "application/vnd.openxmlformats-package." +APP_DOCUMENT = "application/vnd.openxmlformats-officedocument." + +defaults = [ + ["rels", APP_PACKAGE + "relationships+xml"], + ["xml", "application/xml"], +] + +overrides = [ + ["/docProps/app.xml", APP_DOCUMENT + "extended-properties+xml"], + ["/docProps/core.xml", APP_PACKAGE + "core-properties+xml"], + ["/xl/styles.xml", APP_DOCUMENT + "spreadsheetml.styles+xml"], + ["/xl/theme/theme1.xml", APP_DOCUMENT + "theme+xml"], + ["/xl/workbook.xml", APP_DOCUMENT + "spreadsheetml.sheet.main+xml"], +] + + +class ContentTypes(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX ContentTypes file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + # Copy the defaults in case we need to change them. + self.defaults = copy.deepcopy(defaults) + self.overrides = copy.deepcopy(overrides) + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + self._write_types() + self._write_defaults() + self._write_overrides() + + self._xml_end_tag("Types") + + # Close the file. + self._xml_close() + + def _add_default(self, default): + # Add elements to the ContentTypes defaults. + self.defaults.append(default) + + def _add_override(self, override): + # Add elements to the ContentTypes overrides. + self.overrides.append(override) + + def _add_worksheet_name(self, worksheet_name): + # Add the name of a worksheet to the ContentTypes overrides. + worksheet_name = "/xl/worksheets/" + worksheet_name + ".xml" + + self._add_override( + (worksheet_name, APP_DOCUMENT + "spreadsheetml.worksheet+xml") + ) + + def _add_chartsheet_name(self, chartsheet_name): + # Add the name of a chartsheet to the ContentTypes overrides. + chartsheet_name = "/xl/chartsheets/" + chartsheet_name + ".xml" + + self._add_override( + (chartsheet_name, APP_DOCUMENT + "spreadsheetml.chartsheet+xml") + ) + + def _add_chart_name(self, chart_name): + # Add the name of a chart to the ContentTypes overrides. + chart_name = "/xl/charts/" + chart_name + ".xml" + + self._add_override((chart_name, APP_DOCUMENT + "drawingml.chart+xml")) + + def _add_drawing_name(self, drawing_name): + # Add the name of a drawing to the ContentTypes overrides. + drawing_name = "/xl/drawings/" + drawing_name + ".xml" + + self._add_override((drawing_name, APP_DOCUMENT + "drawing+xml")) + + def _add_vml_name(self): + # Add the name of a VML drawing to the ContentTypes defaults. + self._add_default(("vml", APP_DOCUMENT + "vmlDrawing")) + + def _add_comment_name(self, comment_name): + # Add the name of a comment to the ContentTypes overrides. + comment_name = "/xl/" + comment_name + ".xml" + + self._add_override((comment_name, APP_DOCUMENT + "spreadsheetml.comments+xml")) + + def _add_shared_strings(self): + # Add the sharedStrings link to the ContentTypes overrides. + self._add_override( + ("/xl/sharedStrings.xml", APP_DOCUMENT + "spreadsheetml.sharedStrings+xml") + ) + + def _add_calc_chain(self): + # Add the calcChain link to the ContentTypes overrides. + self._add_override( + ("/xl/calcChain.xml", APP_DOCUMENT + "spreadsheetml.calcChain+xml") + ) + + def _add_image_types(self, image_types): + # Add the image default types. + for image_type in image_types: + extension = image_type + + if image_type in ("wmf", "emf"): + image_type = "x-" + image_type + + self._add_default((extension, "image/" + image_type)) + + def _add_table_name(self, table_name): + # Add the name of a table to the ContentTypes overrides. + table_name = "/xl/tables/" + table_name + ".xml" + + self._add_override((table_name, APP_DOCUMENT + "spreadsheetml.table+xml")) + + def _add_vba_project(self): + # Add a vbaProject to the ContentTypes defaults. + + # Change the workbook.xml content-type from xlsx to xlsm. + for i, override in enumerate(self.overrides): + if override[0] == "/xl/workbook.xml": + xlsm = "application/vnd.ms-excel.sheet.macroEnabled.main+xml" + self.overrides[i][1] = xlsm + + self._add_default(("bin", "application/vnd.ms-office.vbaProject")) + + def _add_vba_project_signature(self): + # Add a vbaProjectSignature to the ContentTypes overrides. + self._add_override( + ( + "/xl/vbaProjectSignature.bin", + "application/vnd.ms-office.vbaProjectSignature", + ) + ) + + def _add_custom_properties(self): + # Add the custom properties to the ContentTypes overrides. + self._add_override( + ("/docProps/custom.xml", APP_DOCUMENT + "custom-properties+xml") + ) + + def _add_metadata(self): + # Add the metadata file to the ContentTypes overrides. + self._add_override( + ("/xl/metadata.xml", APP_DOCUMENT + "spreadsheetml.sheetMetadata+xml") + ) + + def _add_feature_bag_property(self): + # Add the featurePropertyBag file to the ContentTypes overrides. + self._add_override( + ( + "/xl/featurePropertyBag/featurePropertyBag.xml", + "application/vnd.ms-excel.featurepropertybag+xml", + ) + ) + + def _add_rich_value(self): + # Add the richValue files to the ContentTypes overrides. + self._add_override( + ( + "/xl/richData/rdRichValueTypes.xml", + "application/vnd.ms-excel.rdrichvaluetypes+xml", + ) + ) + + self._add_override( + ("/xl/richData/rdrichvalue.xml", "application/vnd.ms-excel.rdrichvalue+xml") + ) + + self._add_override( + ( + "/xl/richData/rdrichvaluestructure.xml", + "application/vnd.ms-excel.rdrichvaluestructure+xml", + ) + ) + + self._add_override( + ( + "/xl/richData/richValueRel.xml", + "application/vnd.ms-excel.richvaluerel+xml", + ) + ) + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_defaults(self): + # Write out all of the <Default> types. + + for extension, content_type in self.defaults: + self._xml_empty_tag( + "Default", [("Extension", extension), ("ContentType", content_type)] + ) + + def _write_overrides(self): + # Write out all of the <Override> types. + for part_name, content_type in self.overrides: + self._xml_empty_tag( + "Override", [("PartName", part_name), ("ContentType", content_type)] + ) + + def _write_types(self): + # Write the <Types> element. + xmlns = "http://schemas.openxmlformats.org/package/2006/content-types" + + attributes = [ + ( + "xmlns", + xmlns, + ) + ] + self._xml_start_tag("Types", attributes) + + def _write_default(self, extension, content_type): + # Write the <Default> element. + attributes = [ + ("Extension", extension), + ("ContentType", content_type), + ] + + self._xml_empty_tag("Default", attributes) + + def _write_override(self, part_name, content_type): + # Write the <Override> element. + attributes = [ + ("PartName", part_name), + ("ContentType", content_type), + ] + + self._xml_empty_tag("Override", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/core.py b/.venv/lib/python3.12/site-packages/xlsxwriter/core.py new file mode 100644 index 00000000..114e47a1 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/core.py @@ -0,0 +1,206 @@ +############################################################################### +# +# Core - A class for writing the Excel XLSX Worksheet file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Standard packages. +from datetime import datetime, timezone + +# Package imports. +from . import xmlwriter + + +class Core(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX Core file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + self.properties = {} + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + self._write_cp_core_properties() + self._write_dc_title() + self._write_dc_subject() + self._write_dc_creator() + self._write_cp_keywords() + self._write_dc_description() + self._write_cp_last_modified_by() + self._write_dcterms_created() + self._write_dcterms_modified() + self._write_cp_category() + self._write_cp_content_status() + + self._xml_end_tag("cp:coreProperties") + + # Close the file. + self._xml_close() + + def _set_properties(self, properties): + # Set the document properties. + self.properties = properties + + def _datetime_to_iso8601_date(self, date): + # Convert to a ISO 8601 style "2010-01-01T00:00:00Z" date. + if not date: + date = datetime.now(timezone.utc) + + return date.strftime("%Y-%m-%dT%H:%M:%SZ") + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_cp_core_properties(self): + # Write the <cp:coreProperties> element. + + xmlns_cp = ( + "http://schemas.openxmlformats.org/package/2006/" + + "metadata/core-properties" + ) + xmlns_dc = "http://purl.org/dc/elements/1.1/" + xmlns_dcterms = "http://purl.org/dc/terms/" + xmlns_dcmitype = "http://purl.org/dc/dcmitype/" + xmlns_xsi = "http://www.w3.org/2001/XMLSchema-instance" + + attributes = [ + ("xmlns:cp", xmlns_cp), + ("xmlns:dc", xmlns_dc), + ("xmlns:dcterms", xmlns_dcterms), + ("xmlns:dcmitype", xmlns_dcmitype), + ("xmlns:xsi", xmlns_xsi), + ] + + self._xml_start_tag("cp:coreProperties", attributes) + + def _write_dc_creator(self): + # Write the <dc:creator> element. + data = self.properties.get("author", "") + + self._xml_data_element("dc:creator", data) + + def _write_cp_last_modified_by(self): + # Write the <cp:lastModifiedBy> element. + data = self.properties.get("author", "") + + self._xml_data_element("cp:lastModifiedBy", data) + + def _write_dcterms_created(self): + # Write the <dcterms:created> element. + date = self.properties.get("created", datetime.now(timezone.utc)) + + xsi_type = "dcterms:W3CDTF" + + date = self._datetime_to_iso8601_date(date) + + attributes = [ + ( + "xsi:type", + xsi_type, + ) + ] + + self._xml_data_element("dcterms:created", date, attributes) + + def _write_dcterms_modified(self): + # Write the <dcterms:modified> element. + date = self.properties.get("created", datetime.now(timezone.utc)) + + xsi_type = "dcterms:W3CDTF" + + date = self._datetime_to_iso8601_date(date) + + attributes = [ + ( + "xsi:type", + xsi_type, + ) + ] + + self._xml_data_element("dcterms:modified", date, attributes) + + def _write_dc_title(self): + # Write the <dc:title> element. + if "title" in self.properties: + data = self.properties["title"] + else: + return + + self._xml_data_element("dc:title", data) + + def _write_dc_subject(self): + # Write the <dc:subject> element. + if "subject" in self.properties: + data = self.properties["subject"] + else: + return + + self._xml_data_element("dc:subject", data) + + def _write_cp_keywords(self): + # Write the <cp:keywords> element. + if "keywords" in self.properties: + data = self.properties["keywords"] + else: + return + + self._xml_data_element("cp:keywords", data) + + def _write_dc_description(self): + # Write the <dc:description> element. + if "comments" in self.properties: + data = self.properties["comments"] + else: + return + + self._xml_data_element("dc:description", data) + + def _write_cp_category(self): + # Write the <cp:category> element. + if "category" in self.properties: + data = self.properties["category"] + else: + return + + self._xml_data_element("cp:category", data) + + def _write_cp_content_status(self): + # Write the <cp:contentStatus> element. + if "status" in self.properties: + data = self.properties["status"] + else: + return + + self._xml_data_element("cp:contentStatus", data) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/custom.py b/.venv/lib/python3.12/site-packages/xlsxwriter/custom.py new file mode 100644 index 00000000..400f1645 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/custom.py @@ -0,0 +1,142 @@ +############################################################################### +# +# Custom - A class for writing the Excel XLSX Custom Property file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Package imports. +from . import xmlwriter + + +class Custom(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX Custom Workbook Property file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + self.properties = [] + self.pid = 1 + + def _set_properties(self, properties): + # Set the document properties. + self.properties = properties + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + self._write_properties() + + self._xml_end_tag("Properties") + + # Close the file. + self._xml_close() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_properties(self): + # Write the <Properties> element. + schema = "http://schemas.openxmlformats.org/officeDocument/2006/" + xmlns = schema + "custom-properties" + xmlns_vt = schema + "docPropsVTypes" + + attributes = [ + ("xmlns", xmlns), + ("xmlns:vt", xmlns_vt), + ] + + self._xml_start_tag("Properties", attributes) + + for custom_property in self.properties: + # Write the property element. + self._write_property(custom_property) + + def _write_property(self, custom_property): + # Write the <property> element. + + fmtid = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" + + name, value, property_type = custom_property + self.pid += 1 + + attributes = [ + ("fmtid", fmtid), + ("pid", self.pid), + ("name", name), + ] + + self._xml_start_tag("property", attributes) + + if property_type == "number_int": + # Write the vt:i4 element. + self._write_vt_i4(value) + elif property_type == "number": + # Write the vt:r8 element. + self._write_vt_r8(value) + elif property_type == "date": + # Write the vt:filetime element. + self._write_vt_filetime(value) + elif property_type == "bool": + # Write the vt:bool element. + self._write_vt_bool(value) + else: + # Write the vt:lpwstr element. + self._write_vt_lpwstr(value) + + self._xml_end_tag("property") + + def _write_vt_lpwstr(self, value): + # Write the <vt:lpwstr> element. + self._xml_data_element("vt:lpwstr", value) + + def _write_vt_filetime(self, value): + # Write the <vt:filetime> element. + self._xml_data_element("vt:filetime", value) + + def _write_vt_i4(self, value): + # Write the <vt:i4> element. + self._xml_data_element("vt:i4", value) + + def _write_vt_r8(self, value): + # Write the <vt:r8> element. + self._xml_data_element("vt:r8", value) + + def _write_vt_bool(self, value): + # Write the <vt:bool> element. + + if value: + value = "true" + else: + value = "false" + + self._xml_data_element("vt:bool", value) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/drawing.py b/.venv/lib/python3.12/site-packages/xlsxwriter/drawing.py new file mode 100644 index 00000000..078e532c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/drawing.py @@ -0,0 +1,1196 @@ +############################################################################### +# +# Drawing - A class for writing the Excel XLSX Drawing file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from . import xmlwriter +from .shape import Shape +from .utility import _get_rgb_color + + +class Drawing(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX Drawing file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + self.drawings = [] + self.embedded = 0 + self.orientation = 0 + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + # Write the xdr:wsDr element. + self._write_drawing_workspace() + + if self.embedded: + index = 0 + for drawing_properties in self.drawings: + # Write the xdr:twoCellAnchor element. + index += 1 + self._write_two_cell_anchor(index, drawing_properties) + + else: + # Write the xdr:absoluteAnchor element. + self._write_absolute_anchor(1) + + self._xml_end_tag("xdr:wsDr") + + # Close the file. + self._xml_close() + + def _add_drawing_object(self): + # Add a chart, image or shape sub object to the drawing. + + drawing_object = { + "anchor_type": None, + "dimensions": [], + "width": 0, + "height": 0, + "shape": None, + "anchor": None, + "rel_index": 0, + "url_rel_index": 0, + "tip": None, + "name": None, + "description": None, + "decorative": False, + } + + self.drawings.append(drawing_object) + + return drawing_object + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_drawing_workspace(self): + # Write the <xdr:wsDr> element. + schema = "http://schemas.openxmlformats.org/drawingml/" + xmlns_xdr = schema + "2006/spreadsheetDrawing" + xmlns_a = schema + "2006/main" + + attributes = [ + ("xmlns:xdr", xmlns_xdr), + ("xmlns:a", xmlns_a), + ] + + self._xml_start_tag("xdr:wsDr", attributes) + + def _write_two_cell_anchor(self, index, drawing_properties): + # Write the <xdr:twoCellAnchor> element. + anchor_type = drawing_properties["type"] + dimensions = drawing_properties["dimensions"] + col_from = dimensions[0] + row_from = dimensions[1] + col_from_offset = dimensions[2] + row_from_offset = dimensions[3] + col_to = dimensions[4] + row_to = dimensions[5] + col_to_offset = dimensions[6] + row_to_offset = dimensions[7] + col_absolute = dimensions[8] + row_absolute = dimensions[9] + width = drawing_properties["width"] + height = drawing_properties["height"] + shape = drawing_properties["shape"] + anchor = drawing_properties["anchor"] + rel_index = drawing_properties["rel_index"] + url_rel_index = drawing_properties["url_rel_index"] + tip = drawing_properties["tip"] + name = drawing_properties["name"] + description = drawing_properties["description"] + decorative = drawing_properties["decorative"] + + attributes = [] + + # Add attribute for positioning. + if anchor == 2: + attributes.append(("editAs", "oneCell")) + elif anchor == 3: + attributes.append(("editAs", "absolute")) + + # Add editAs attribute for shapes. + if shape and shape.edit_as: + attributes.append(("editAs", shape.edit_as)) + + self._xml_start_tag("xdr:twoCellAnchor", attributes) + + # Write the xdr:from element. + self._write_from(col_from, row_from, col_from_offset, row_from_offset) + + # Write the xdr:from element. + self._write_to(col_to, row_to, col_to_offset, row_to_offset) + + if anchor_type == 1: + # Graphic frame. + # Write the xdr:graphicFrame element for charts. + self._write_graphic_frame(index, rel_index, name, description, decorative) + elif anchor_type == 2: + # Write the xdr:pic element. + self._write_pic( + index, + rel_index, + col_absolute, + row_absolute, + width, + height, + shape, + description, + url_rel_index, + tip, + decorative, + ) + else: + # Write the xdr:sp element for shapes. + self._write_sp( + index, + col_absolute, + row_absolute, + width, + height, + shape, + description, + url_rel_index, + tip, + decorative, + ) + + # Write the xdr:clientData element. + self._write_client_data() + + self._xml_end_tag("xdr:twoCellAnchor") + + def _write_absolute_anchor(self, frame_index): + self._xml_start_tag("xdr:absoluteAnchor") + # Write the <xdr:absoluteAnchor> element. + + # Different coordinates for horizontal (= 0) and vertical (= 1). + if self.orientation == 0: + # Write the xdr:pos element. + self._write_pos(0, 0) + + # Write the xdr:ext element. + self._write_xdr_ext(9308969, 6078325) + + else: + # Write the xdr:pos element. + self._write_pos(0, -47625) + + # Write the xdr:ext element. + self._write_xdr_ext(6162675, 6124575) + + # Write the xdr:graphicFrame element. + self._write_graphic_frame(frame_index, frame_index) + + # Write the xdr:clientData element. + self._write_client_data() + + self._xml_end_tag("xdr:absoluteAnchor") + + def _write_from(self, col, row, col_offset, row_offset): + # Write the <xdr:from> element. + self._xml_start_tag("xdr:from") + + # Write the xdr:col element. + self._write_col(col) + + # Write the xdr:colOff element. + self._write_col_off(col_offset) + + # Write the xdr:row element. + self._write_row(row) + + # Write the xdr:rowOff element. + self._write_row_off(row_offset) + + self._xml_end_tag("xdr:from") + + def _write_to(self, col, row, col_offset, row_offset): + # Write the <xdr:to> element. + self._xml_start_tag("xdr:to") + + # Write the xdr:col element. + self._write_col(col) + + # Write the xdr:colOff element. + self._write_col_off(col_offset) + + # Write the xdr:row element. + self._write_row(row) + + # Write the xdr:rowOff element. + self._write_row_off(row_offset) + + self._xml_end_tag("xdr:to") + + def _write_col(self, data): + # Write the <xdr:col> element. + self._xml_data_element("xdr:col", data) + + def _write_col_off(self, data): + # Write the <xdr:colOff> element. + self._xml_data_element("xdr:colOff", data) + + def _write_row(self, data): + # Write the <xdr:row> element. + self._xml_data_element("xdr:row", data) + + def _write_row_off(self, data): + # Write the <xdr:rowOff> element. + self._xml_data_element("xdr:rowOff", data) + + def _write_pos(self, x, y): + # Write the <xdr:pos> element. + + attributes = [("x", x), ("y", y)] + + self._xml_empty_tag("xdr:pos", attributes) + + def _write_xdr_ext(self, cx, cy): + # Write the <xdr:ext> element. + + attributes = [("cx", cx), ("cy", cy)] + + self._xml_empty_tag("xdr:ext", attributes) + + def _write_graphic_frame( + self, index, rel_index, name=None, description=None, decorative=None + ): + # Write the <xdr:graphicFrame> element. + attributes = [("macro", "")] + + self._xml_start_tag("xdr:graphicFrame", attributes) + + # Write the xdr:nvGraphicFramePr element. + self._write_nv_graphic_frame_pr(index, name, description, decorative) + + # Write the xdr:xfrm element. + self._write_xfrm() + + # Write the a:graphic element. + self._write_atag_graphic(rel_index) + + self._xml_end_tag("xdr:graphicFrame") + + def _write_nv_graphic_frame_pr(self, index, name, description, decorative): + # Write the <xdr:nvGraphicFramePr> element. + + if not name: + name = "Chart " + str(index) + + self._xml_start_tag("xdr:nvGraphicFramePr") + + # Write the xdr:cNvPr element. + self._write_c_nv_pr(index + 1, name, description, None, None, decorative) + + # Write the xdr:cNvGraphicFramePr element. + self._write_c_nv_graphic_frame_pr() + + self._xml_end_tag("xdr:nvGraphicFramePr") + + def _write_c_nv_pr(self, index, name, description, url_rel_index, tip, decorative): + # Write the <xdr:cNvPr> element. + attributes = [("id", index), ("name", name)] + + # Add description attribute for images. + if description and not decorative: + attributes.append(("descr", description)) + + if url_rel_index or decorative: + self._xml_start_tag("xdr:cNvPr", attributes) + + if url_rel_index: + self._write_a_hlink_click(url_rel_index, tip) + + if decorative: + self._write_decorative() + + self._xml_end_tag("xdr:cNvPr") + else: + self._xml_empty_tag("xdr:cNvPr", attributes) + + def _write_decorative(self): + self._xml_start_tag("a:extLst") + + self._write_uri_ext("{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}") + self._write_a16_creation_id() + self._xml_end_tag("a:ext") + + self._write_uri_ext("{C183D7F6-B498-43B3-948B-1728B52AA6E4}") + self._write_adec_decorative() + self._xml_end_tag("a:ext") + + self._xml_end_tag("a:extLst") + + def _write_uri_ext(self, uri): + # Write the <a:ext> element. + attributes = [("uri", uri)] + + self._xml_start_tag("a:ext", attributes) + + def _write_adec_decorative(self): + # Write the <adec:decorative> element. + xmlns = "http://schemas.microsoft.com/office/drawing/2017/decorative" + val = "1" + + attributes = [ + ("xmlns:adec", xmlns), + ("val", val), + ] + + self._xml_empty_tag("adec:decorative", attributes) + + def _write_a16_creation_id(self): + # Write the <a16:creationId> element. + + xmlns_a_16 = "http://schemas.microsoft.com/office/drawing/2014/main" + creation_id = "{00000000-0008-0000-0000-000002000000}" + + attributes = [ + ("xmlns:a16", xmlns_a_16), + ("id", creation_id), + ] + + self._xml_empty_tag("a16:creationId", attributes) + + def _write_a_hlink_click(self, rel_index, tip): + # Write the <a:hlinkClick> element. + schema = "http://schemas.openxmlformats.org/officeDocument/" + xmlns_r = schema + "2006/relationships" + + attributes = [ + ("xmlns:r", xmlns_r), + ("r:id", "rId" + str(rel_index)), + ] + + if tip: + attributes.append(("tooltip", tip)) + + self._xml_empty_tag("a:hlinkClick", attributes) + + def _write_c_nv_graphic_frame_pr(self): + # Write the <xdr:cNvGraphicFramePr> element. + if self.embedded: + self._xml_empty_tag("xdr:cNvGraphicFramePr") + else: + self._xml_start_tag("xdr:cNvGraphicFramePr") + + # Write the a:graphicFrameLocks element. + self._write_a_graphic_frame_locks() + + self._xml_end_tag("xdr:cNvGraphicFramePr") + + def _write_a_graphic_frame_locks(self): + # Write the <a:graphicFrameLocks> element. + attributes = [("noGrp", 1)] + + self._xml_empty_tag("a:graphicFrameLocks", attributes) + + def _write_xfrm(self): + # Write the <xdr:xfrm> element. + self._xml_start_tag("xdr:xfrm") + + # Write the xfrmOffset element. + self._write_xfrm_offset() + + # Write the xfrmOffset element. + self._write_xfrm_extension() + + self._xml_end_tag("xdr:xfrm") + + def _write_xfrm_offset(self): + # Write the <a:off> xfrm sub-element. + + attributes = [ + ("x", 0), + ("y", 0), + ] + + self._xml_empty_tag("a:off", attributes) + + def _write_xfrm_extension(self): + # Write the <a:ext> xfrm sub-element. + + attributes = [ + ("cx", 0), + ("cy", 0), + ] + + self._xml_empty_tag("a:ext", attributes) + + def _write_atag_graphic(self, index): + # Write the <a:graphic> element. + self._xml_start_tag("a:graphic") + + # Write the a:graphicData element. + self._write_atag_graphic_data(index) + + self._xml_end_tag("a:graphic") + + def _write_atag_graphic_data(self, index): + # Write the <a:graphicData> element. + uri = "http://schemas.openxmlformats.org/drawingml/2006/chart" + + attributes = [ + ( + "uri", + uri, + ) + ] + + self._xml_start_tag("a:graphicData", attributes) + + # Write the c:chart element. + self._write_c_chart("rId" + str(index)) + + self._xml_end_tag("a:graphicData") + + def _write_c_chart(self, r_id): + # Write the <c:chart> element. + + schema = "http://schemas.openxmlformats.org/" + xmlns_c = schema + "drawingml/2006/chart" + xmlns_r = schema + "officeDocument/2006/relationships" + + attributes = [ + ("xmlns:c", xmlns_c), + ("xmlns:r", xmlns_r), + ("r:id", r_id), + ] + + self._xml_empty_tag("c:chart", attributes) + + def _write_client_data(self): + # Write the <xdr:clientData> element. + self._xml_empty_tag("xdr:clientData") + + def _write_sp( + self, + index, + col_absolute, + row_absolute, + width, + height, + shape, + description, + url_rel_index, + tip, + decorative, + ): + # Write the <xdr:sp> element. + + if shape and shape.connect: + attributes = [("macro", "")] + self._xml_start_tag("xdr:cxnSp", attributes) + + # Write the xdr:nvCxnSpPr element. + self._write_nv_cxn_sp_pr(index, shape) + + # Write the xdr:spPr element. + self._write_xdr_sp_pr(col_absolute, row_absolute, width, height, shape) + + self._xml_end_tag("xdr:cxnSp") + else: + # Add attribute for shapes. + attributes = [("macro", ""), ("textlink", shape.textlink)] + + self._xml_start_tag("xdr:sp", attributes) + + # Write the xdr:nvSpPr element. + self._write_nv_sp_pr( + index, shape, url_rel_index, tip, description, decorative + ) + + # Write the xdr:spPr element. + self._write_xdr_sp_pr(col_absolute, row_absolute, width, height, shape) + + # Write the xdr:style element. + self._write_style() + + # Write the xdr:txBody element. + if shape.text is not None: + self._write_tx_body(shape) + + self._xml_end_tag("xdr:sp") + + def _write_nv_cxn_sp_pr(self, index, shape): + # Write the <xdr:nvCxnSpPr> element. + self._xml_start_tag("xdr:nvCxnSpPr") + + name = shape.name + " " + str(index) + if name is not None: + self._write_c_nv_pr(index, name, None, None, None, None) + + self._xml_start_tag("xdr:cNvCxnSpPr") + + attributes = [("noChangeShapeType", "1")] + self._xml_empty_tag("a:cxnSpLocks", attributes) + + if shape.start: + attributes = [("id", shape.start), ("idx", shape.start_index)] + self._xml_empty_tag("a:stCxn", attributes) + + if shape.end: + attributes = [("id", shape.end), ("idx", shape.end_index)] + self._xml_empty_tag("a:endCxn", attributes) + + self._xml_end_tag("xdr:cNvCxnSpPr") + self._xml_end_tag("xdr:nvCxnSpPr") + + def _write_nv_sp_pr( + self, index, shape, url_rel_index, tip, description, decorative + ): + # Write the <xdr:NvSpPr> element. + attributes = [] + + self._xml_start_tag("xdr:nvSpPr") + + name = shape.name + " " + str(index) + + self._write_c_nv_pr( + index + 1, name, description, url_rel_index, tip, decorative + ) + + if shape.name == "TextBox": + attributes = [("txBox", 1)] + + self._xml_empty_tag("xdr:cNvSpPr", attributes) + + self._xml_end_tag("xdr:nvSpPr") + + def _write_pic( + self, + index, + rel_index, + col_absolute, + row_absolute, + width, + height, + shape, + description, + url_rel_index, + tip, + decorative, + ): + # Write the <xdr:pic> element. + self._xml_start_tag("xdr:pic") + + # Write the xdr:nvPicPr element. + self._write_nv_pic_pr(index, description, url_rel_index, tip, decorative) + # Write the xdr:blipFill element. + self._write_blip_fill(rel_index) + + # Write the xdr:spPr element. + self._write_sp_pr(col_absolute, row_absolute, width, height, shape) + + self._xml_end_tag("xdr:pic") + + def _write_nv_pic_pr(self, index, description, url_rel_index, tip, decorative): + # Write the <xdr:nvPicPr> element. + self._xml_start_tag("xdr:nvPicPr") + + # Write the xdr:cNvPr element. + self._write_c_nv_pr( + index + 1, + "Picture " + str(index), + description, + url_rel_index, + tip, + decorative, + ) + + # Write the xdr:cNvPicPr element. + self._write_c_nv_pic_pr() + + self._xml_end_tag("xdr:nvPicPr") + + def _write_c_nv_pic_pr(self): + # Write the <xdr:cNvPicPr> element. + self._xml_start_tag("xdr:cNvPicPr") + + # Write the a:picLocks element. + self._write_a_pic_locks() + + self._xml_end_tag("xdr:cNvPicPr") + + def _write_a_pic_locks(self): + # Write the <a:picLocks> element. + attributes = [("noChangeAspect", 1)] + + self._xml_empty_tag("a:picLocks", attributes) + + def _write_blip_fill(self, index): + # Write the <xdr:blipFill> element. + self._xml_start_tag("xdr:blipFill") + + # Write the a:blip element. + self._write_a_blip(index) + + # Write the a:stretch element. + self._write_a_stretch() + + self._xml_end_tag("xdr:blipFill") + + def _write_a_blip(self, index): + # Write the <a:blip> element. + schema = "http://schemas.openxmlformats.org/officeDocument/" + xmlns_r = schema + "2006/relationships" + r_embed = "rId" + str(index) + + attributes = [("xmlns:r", xmlns_r), ("r:embed", r_embed)] + + self._xml_empty_tag("a:blip", attributes) + + def _write_a_stretch(self): + # Write the <a:stretch> element. + self._xml_start_tag("a:stretch") + + # Write the a:fillRect element. + self._write_a_fill_rect() + + self._xml_end_tag("a:stretch") + + def _write_a_fill_rect(self): + # Write the <a:fillRect> element. + self._xml_empty_tag("a:fillRect") + + def _write_sp_pr(self, col_absolute, row_absolute, width, height, shape=None): + # Write the <xdr:spPr> element, for charts. + + self._xml_start_tag("xdr:spPr") + + # Write the a:xfrm element. + self._write_a_xfrm(col_absolute, row_absolute, width, height) + + # Write the a:prstGeom element. + self._write_a_prst_geom(shape) + + self._xml_end_tag("xdr:spPr") + + def _write_xdr_sp_pr(self, col_absolute, row_absolute, width, height, shape): + # Write the <xdr:spPr> element for shapes. + self._xml_start_tag("xdr:spPr") + + # Write the a:xfrm element. + self._write_a_xfrm(col_absolute, row_absolute, width, height, shape) + + # Write the a:prstGeom element. + self._write_a_prst_geom(shape) + + if shape.fill: + if not shape.fill["defined"]: + # Write the a:solidFill element. + self._write_a_solid_fill_scheme("lt1") + elif "none" in shape.fill: + # Write the a:noFill element. + self._xml_empty_tag("a:noFill") + elif "color" in shape.fill: + # Write the a:solidFill element. + self._write_a_solid_fill(_get_rgb_color(shape.fill["color"])) + + if shape.gradient: + # Write the a:gradFill element. + self._write_a_grad_fill(shape.gradient) + + # Write the a:ln element. + self._write_a_ln(shape.line) + + self._xml_end_tag("xdr:spPr") + + def _write_a_xfrm(self, col_absolute, row_absolute, width, height, shape=None): + # Write the <a:xfrm> element. + attributes = [] + + if shape: + if shape.rotation: + rotation = shape.rotation + rotation *= 60000 + attributes.append(("rot", rotation)) + + if shape.flip_h: + attributes.append(("flipH", 1)) + if shape.flip_v: + attributes.append(("flipV", 1)) + + self._xml_start_tag("a:xfrm", attributes) + + # Write the a:off element. + self._write_a_off(col_absolute, row_absolute) + + # Write the a:ext element. + self._write_a_ext(width, height) + + self._xml_end_tag("a:xfrm") + + def _write_a_off(self, x, y): + # Write the <a:off> element. + attributes = [ + ("x", x), + ("y", y), + ] + + self._xml_empty_tag("a:off", attributes) + + def _write_a_ext(self, cx, cy): + # Write the <a:ext> element. + attributes = [ + ("cx", cx), + ("cy", cy), + ] + + self._xml_empty_tag("a:ext", attributes) + + def _write_a_prst_geom(self, shape=None): + # Write the <a:prstGeom> element. + attributes = [("prst", "rect")] + + self._xml_start_tag("a:prstGeom", attributes) + + # Write the a:avLst element. + self._write_a_av_lst(shape) + + self._xml_end_tag("a:prstGeom") + + def _write_a_av_lst(self, shape=None): + # Write the <a:avLst> element. + adjustments = [] + + if shape and shape.adjustments: + adjustments = shape.adjustments + + if adjustments: + self._xml_start_tag("a:avLst") + + i = 0 + for adj in adjustments: + i += 1 + # Only connectors have multiple adjustments. + if shape.connect: + suffix = i + else: + suffix = "" + + # Scale Adjustments: 100,000 = 100%. + adj_int = str(int(adj * 1000)) + + attributes = [("name", "adj" + suffix), ("fmla", "val" + adj_int)] + + self._xml_empty_tag("a:gd", attributes) + + self._xml_end_tag("a:avLst") + else: + self._xml_empty_tag("a:avLst") + + def _write_a_solid_fill(self, rgb): + # Write the <a:solidFill> element. + if rgb is None: + rgb = "FFFFFF" + + self._xml_start_tag("a:solidFill") + + # Write the a:srgbClr element. + self._write_a_srgb_clr(rgb) + + self._xml_end_tag("a:solidFill") + + def _write_a_solid_fill_scheme(self, color, shade=None): + attributes = [("val", color)] + + self._xml_start_tag("a:solidFill") + + if shade: + self._xml_start_tag("a:schemeClr", attributes) + self._write_a_shade(shade) + self._xml_end_tag("a:schemeClr") + else: + self._xml_empty_tag("a:schemeClr", attributes) + + self._xml_end_tag("a:solidFill") + + def _write_a_ln(self, line): + # Write the <a:ln> element. + width = line.get("width", 0.75) + + # Round width to nearest 0.25, like Excel. + width = int((width + 0.125) * 4) / 4.0 + + # Convert to internal units. + width = int(0.5 + (12700 * width)) + + attributes = [("w", width), ("cmpd", "sng")] + + self._xml_start_tag("a:ln", attributes) + + if "none" in line: + # Write the a:noFill element. + self._xml_empty_tag("a:noFill") + + elif "color" in line: + # Write the a:solidFill element. + self._write_a_solid_fill(_get_rgb_color(line["color"])) + + else: + # Write the a:solidFill element. + self._write_a_solid_fill_scheme("lt1", "50000") + + # Write the line/dash type. + line_type = line.get("dash_type") + if line_type: + # Write the a:prstDash element. + self._write_a_prst_dash(line_type) + + self._xml_end_tag("a:ln") + + def _write_tx_body(self, shape): + # Write the <xdr:txBody> element. + attributes = [] + + if shape.text_rotation != 0: + if shape.text_rotation == 90: + attributes.append(("vert", "vert270")) + if shape.text_rotation == -90: + attributes.append(("vert", "vert")) + if shape.text_rotation == 270: + attributes.append(("vert", "wordArtVert")) + if shape.text_rotation == 271: + attributes.append(("vert", "eaVert")) + + attributes.append(("wrap", "square")) + attributes.append(("rtlCol", "0")) + + if not shape.align["defined"]: + attributes.append(("anchor", "t")) + else: + if "vertical" in shape.align: + align = shape.align["vertical"] + if align == "top": + attributes.append(("anchor", "t")) + elif align == "middle": + attributes.append(("anchor", "ctr")) + elif align == "bottom": + attributes.append(("anchor", "b")) + else: + attributes.append(("anchor", "t")) + + if "horizontal" in shape.align: + align = shape.align["horizontal"] + if align == "center": + attributes.append(("anchorCtr", "1")) + else: + attributes.append(("anchorCtr", "0")) + + self._xml_start_tag("xdr:txBody") + self._xml_empty_tag("a:bodyPr", attributes) + self._xml_empty_tag("a:lstStyle") + + lines = shape.text.split("\n") + + # Set the font attributes. + font = shape.font + # pylint: disable=protected-access + style_attrs = Shape._get_font_style_attributes(font) + latin_attrs = Shape._get_font_latin_attributes(font) + style_attrs.insert(0, ("lang", font["lang"])) + + if shape.textlink != "": + attributes = [ + ("id", "{B8ADDEFE-BF52-4FD4-8C5D-6B85EF6FF707}"), + ("type", "TxLink"), + ] + + self._xml_start_tag("a:p") + self._xml_start_tag("a:fld", attributes) + + self._write_font_run(font, style_attrs, latin_attrs, "a:rPr") + + self._xml_data_element("a:t", shape.text) + self._xml_end_tag("a:fld") + + self._write_font_run(font, style_attrs, latin_attrs, "a:endParaRPr") + + self._xml_end_tag("a:p") + else: + for line in lines: + self._xml_start_tag("a:p") + + if line == "": + self._write_font_run(font, style_attrs, latin_attrs, "a:endParaRPr") + self._xml_end_tag("a:p") + continue + + if "text" in shape.align: + if shape.align["text"] == "left": + self._xml_empty_tag("a:pPr", [("algn", "l")]) + if shape.align["text"] == "center": + self._xml_empty_tag("a:pPr", [("algn", "ctr")]) + if shape.align["text"] == "right": + self._xml_empty_tag("a:pPr", [("algn", "r")]) + + self._xml_start_tag("a:r") + + self._write_font_run(font, style_attrs, latin_attrs, "a:rPr") + + self._xml_data_element("a:t", line) + + self._xml_end_tag("a:r") + self._xml_end_tag("a:p") + + self._xml_end_tag("xdr:txBody") + + def _write_font_run(self, font, style_attrs, latin_attrs, run_type): + # Write a:rPr or a:endParaRPr. + has_color = font.get("color") is not None + + if latin_attrs or has_color: + self._xml_start_tag(run_type, style_attrs) + + if has_color: + self._write_a_solid_fill(_get_rgb_color(font["color"])) + + if latin_attrs: + self._write_a_latin(latin_attrs) + self._write_a_cs(latin_attrs) + + self._xml_end_tag(run_type) + else: + self._xml_empty_tag(run_type, style_attrs) + + def _write_style(self): + # Write the <xdr:style> element. + self._xml_start_tag("xdr:style") + + # Write the a:lnRef element. + self._write_a_ln_ref() + + # Write the a:fillRef element. + self._write_a_fill_ref() + + # Write the a:effectRef element. + self._write_a_effect_ref() + + # Write the a:fontRef element. + self._write_a_font_ref() + + self._xml_end_tag("xdr:style") + + def _write_a_ln_ref(self): + # Write the <a:lnRef> element. + attributes = [("idx", "0")] + + self._xml_start_tag("a:lnRef", attributes) + + # Write the a:scrgbClr element. + self._write_a_scrgb_clr() + + self._xml_end_tag("a:lnRef") + + def _write_a_fill_ref(self): + # Write the <a:fillRef> element. + attributes = [("idx", "0")] + + self._xml_start_tag("a:fillRef", attributes) + + # Write the a:scrgbClr element. + self._write_a_scrgb_clr() + + self._xml_end_tag("a:fillRef") + + def _write_a_effect_ref(self): + # Write the <a:effectRef> element. + attributes = [("idx", "0")] + + self._xml_start_tag("a:effectRef", attributes) + + # Write the a:scrgbClr element. + self._write_a_scrgb_clr() + + self._xml_end_tag("a:effectRef") + + def _write_a_scrgb_clr(self): + # Write the <a:scrgbClr> element. + + attributes = [ + ("r", "0"), + ("g", "0"), + ("b", "0"), + ] + + self._xml_empty_tag("a:scrgbClr", attributes) + + def _write_a_font_ref(self): + # Write the <a:fontRef> element. + attributes = [("idx", "minor")] + + self._xml_start_tag("a:fontRef", attributes) + + # Write the a:schemeClr element. + self._write_a_scheme_clr("dk1") + + self._xml_end_tag("a:fontRef") + + def _write_a_scheme_clr(self, val): + # Write the <a:schemeClr> element. + attributes = [("val", val)] + + self._xml_empty_tag("a:schemeClr", attributes) + + def _write_a_shade(self, shade): + # Write the <a:shade> element. + attributes = [("val", shade)] + + self._xml_empty_tag("a:shade", attributes) + + def _write_a_prst_dash(self, val): + # Write the <a:prstDash> element. + + attributes = [("val", val)] + + self._xml_empty_tag("a:prstDash", attributes) + + def _write_a_grad_fill(self, gradient): + # Write the <a:gradFill> element. + + attributes = [("flip", "none"), ("rotWithShape", "1")] + + if gradient["type"] == "linear": + attributes = [] + + self._xml_start_tag("a:gradFill", attributes) + + # Write the a:gsLst element. + self._write_a_gs_lst(gradient) + + if gradient["type"] == "linear": + # Write the a:lin element. + self._write_a_lin(gradient["angle"]) + else: + # Write the a:path element. + self._write_a_path(gradient["type"]) + + # Write the a:tileRect element. + self._write_a_tile_rect(gradient["type"]) + + self._xml_end_tag("a:gradFill") + + def _write_a_gs_lst(self, gradient): + # Write the <a:gsLst> element. + positions = gradient["positions"] + colors = gradient["colors"] + + self._xml_start_tag("a:gsLst") + + for i, color in enumerate(colors): + pos = int(positions[i] * 1000) + attributes = [("pos", pos)] + self._xml_start_tag("a:gs", attributes) + + # Write the a:srgbClr element. + color = _get_rgb_color(color) + self._write_a_srgb_clr(color) + + self._xml_end_tag("a:gs") + + self._xml_end_tag("a:gsLst") + + def _write_a_lin(self, angle): + # Write the <a:lin> element. + + angle = int(60000 * angle) + + attributes = [ + ("ang", angle), + ("scaled", "0"), + ] + + self._xml_empty_tag("a:lin", attributes) + + def _write_a_path(self, gradient_type): + # Write the <a:path> element. + + attributes = [("path", gradient_type)] + + self._xml_start_tag("a:path", attributes) + + # Write the a:fillToRect element. + self._write_a_fill_to_rect(gradient_type) + + self._xml_end_tag("a:path") + + def _write_a_fill_to_rect(self, gradient_type): + # Write the <a:fillToRect> element. + + if gradient_type == "shape": + attributes = [ + ("l", "50000"), + ("t", "50000"), + ("r", "50000"), + ("b", "50000"), + ] + else: + attributes = [ + ("l", "100000"), + ("t", "100000"), + ] + + self._xml_empty_tag("a:fillToRect", attributes) + + def _write_a_tile_rect(self, gradient_type): + # Write the <a:tileRect> element. + + if gradient_type == "shape": + attributes = [] + else: + attributes = [ + ("r", "-100000"), + ("b", "-100000"), + ] + + self._xml_empty_tag("a:tileRect", attributes) + + def _write_a_srgb_clr(self, val): + # Write the <a:srgbClr> element. + + attributes = [("val", val)] + + self._xml_empty_tag("a:srgbClr", attributes) + + def _write_a_latin(self, attributes): + # Write the <a:latin> element. + self._xml_empty_tag("a:latin", attributes) + + def _write_a_cs(self, attributes): + # Write the <a:latin> element. + self._xml_empty_tag("a:cs", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/exceptions.py b/.venv/lib/python3.12/site-packages/xlsxwriter/exceptions.py new file mode 100644 index 00000000..6c8198b3 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/exceptions.py @@ -0,0 +1,56 @@ +############################################################################### +# +# Exceptions - A class for XlsxWriter exceptions. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + + +class XlsxWriterException(Exception): + """Base exception for XlsxWriter.""" + + +class XlsxInputError(XlsxWriterException): + """Base exception for all input data related errors.""" + + +class XlsxFileError(XlsxWriterException): + """Base exception for all file related errors.""" + + +class EmptyChartSeries(XlsxInputError): + """Chart must contain at least one data series.""" + + +class DuplicateTableName(XlsxInputError): + """Worksheet table name already exists.""" + + +class InvalidWorksheetName(XlsxInputError): + """Worksheet name is too long or contains restricted characters.""" + + +class DuplicateWorksheetName(XlsxInputError): + """Worksheet name already exists.""" + + +class OverlappingRange(XlsxInputError): + """Worksheet merge range or table overlaps previous range.""" + + +class UndefinedImageSize(XlsxFileError): + """No size data found in image file.""" + + +class UnsupportedImageFormat(XlsxFileError): + """Unsupported image file format.""" + + +class FileCreateError(XlsxFileError): + """IO error when creating xlsx file.""" + + +class FileSizeError(XlsxFileError): + """Filesize would require ZIP64 extensions. Use workbook.use_zip64().""" diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/feature_property_bag.py b/.venv/lib/python3.12/site-packages/xlsxwriter/feature_property_bag.py new file mode 100644 index 00000000..d38bcc7c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/feature_property_bag.py @@ -0,0 +1,156 @@ +############################################################################### +# +# FeaturePropertyBag - A class for writing the Excel XLSX featurePropertyBag.xml +# file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Package imports. +from . import xmlwriter + + +class FeaturePropertyBag(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX FeaturePropertyBag file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + self.feature_property_bags = set() + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + # Write the FeaturePropertyBags element. + self._write_feature_property_bags() + + # Write the Checkbox bag element. + self._write_checkbox_bag() + + # Write the XFControls bag element. + self._write_xf_control_bag() + + # Write the XFComplement bag element. + self._write_xf_compliment_bag() + + # Write the XFComplements bag element. + self._write_xf_compliments_bag() + + # Write the DXFComplements bag element. + if "DXFComplements" in self.feature_property_bags: + self._write_dxf_compliments_bag() + + self._xml_end_tag("FeaturePropertyBags") + + # Close the file. + self._xml_close() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_feature_property_bags(self): + # Write the <FeaturePropertyBags> element. + + xmlns = ( + "http://schemas.microsoft.com/office/spreadsheetml/2022/featurepropertybag" + ) + + attributes = [("xmlns", xmlns)] + + self._xml_start_tag("FeaturePropertyBags", attributes) + + def _write_checkbox_bag(self): + # Write the Checkbox <bag> element. + attributes = [("type", "Checkbox")] + + self._xml_empty_tag("bag", attributes) + + def _write_xf_control_bag(self): + # Write the XFControls<bag> element. + attributes = [("type", "XFControls")] + + self._xml_start_tag("bag", attributes) + + # Write the bagId element. + self._write_bag_id("CellControl", 0) + + self._xml_end_tag("bag") + + def _write_xf_compliment_bag(self): + # Write the XFComplement <bag> element. + attributes = [("type", "XFComplement")] + + self._xml_start_tag("bag", attributes) + + # Write the bagId element. + self._write_bag_id("XFControls", 1) + + self._xml_end_tag("bag") + + def _write_xf_compliments_bag(self): + # Write the XFComplements <bag> element. + attributes = [ + ("type", "XFComplements"), + ("extRef", "XFComplementsMapperExtRef"), + ] + + self._xml_start_tag("bag", attributes) + self._xml_start_tag("a", [("k", "MappedFeaturePropertyBags")]) + + self._write_bag_id("", 2) + + self._xml_end_tag("a") + self._xml_end_tag("bag") + + def _write_dxf_compliments_bag(self): + # Write the DXFComplements <bag> element. + attributes = [ + ("type", "DXFComplements"), + ("extRef", "DXFComplementsMapperExtRef"), + ] + + self._xml_start_tag("bag", attributes) + self._xml_start_tag("a", [("k", "MappedFeaturePropertyBags")]) + + self._write_bag_id("", 2) + + self._xml_end_tag("a") + self._xml_end_tag("bag") + + def _write_bag_id(self, key, bag_id): + # Write the <bagId> element. + attributes = [] + + if key: + attributes = [("k", key)] + + self._xml_data_element("bagId", bag_id, attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/format.py b/.venv/lib/python3.12/site-packages/xlsxwriter/format.py new file mode 100644 index 00000000..22996fed --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/format.py @@ -0,0 +1,1217 @@ +############################################################################### +# +# Format - A class for writing the Excel XLSX Worksheet file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Package imports. +from warnings import warn + +from . import xmlwriter + + +class Format(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX Format file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self, properties=None, xf_indices=None, dxf_indices=None): + """ + Constructor. + + """ + if properties is None: + properties = {} + + super().__init__() + + self.xf_format_indices = xf_indices + self.dxf_format_indices = dxf_indices + self.xf_index = None + self.dxf_index = None + + self.num_format = "General" + self.num_format_index = 0 + self.font_index = 0 + self.has_font = 0 + self.has_dxf_font = 0 + + self.bold = 0 + self.underline = 0 + self.italic = 0 + self.font_name = "Calibri" + self.font_size = 11 + self.font_color = 0x0 + self.font_strikeout = 0 + self.font_outline = 0 + self.font_shadow = 0 + self.font_script = 0 + self.font_family = 2 + self.font_charset = 0 + self.font_scheme = "minor" + self.font_condense = 0 + self.font_extend = 0 + self.theme = 0 + self.hyperlink = False + self.xf_id = 0 + + self.hidden = 0 + self.locked = 1 + + self.text_h_align = 0 + self.text_wrap = 0 + self.text_v_align = 0 + self.text_justlast = 0 + self.rotation = 0 + + self.fg_color = 0 + self.bg_color = 0 + self.pattern = 0 + self.has_fill = 0 + self.has_dxf_fill = 0 + self.fill_index = 0 + self.fill_count = 0 + + self.border_index = 0 + self.has_border = 0 + self.has_dxf_border = 0 + self.border_count = 0 + + self.bottom = 0 + self.bottom_color = 0 + self.diag_border = 0 + self.diag_color = 0 + self.diag_type = 0 + self.left = 0 + self.left_color = 0 + self.right = 0 + self.right_color = 0 + self.top = 0 + self.top_color = 0 + + self.indent = 0 + self.shrink = 0 + self.merge_range = 0 + self.reading_order = 0 + self.just_distrib = 0 + self.color_indexed = 0 + self.font_only = 0 + + self.quote_prefix = False + self.checkbox = False + + # Convert properties in the constructor to method calls. + for key, value in properties.items(): + getattr(self, "set_" + key)(value) + + self._format_key = None + + ########################################################################### + # + # Format properties. + # + ########################################################################### + + def set_font_name(self, font_name): + """ + Set the Format font_name property such as 'Time New Roman'. The + default Excel font is 'Calibri'. + + Args: + font_name: String with the font name. No default. + + Returns: + Nothing. + + """ + self.font_name = font_name + + def set_font_size(self, font_size=11): + """ + Set the Format font_size property. The default Excel font size is 11. + + Args: + font_size: Int with font size. No default. + + Returns: + Nothing. + + """ + self.font_size = font_size + + def set_font_color(self, font_color): + """ + Set the Format font_color property. The Excel default is black. + + Args: + font_color: String with the font color. No default. + + Returns: + Nothing. + + """ + self.font_color = self._get_color(font_color) + + def set_bold(self, bold=True): + """ + Set the Format bold property. + + Args: + bold: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.bold = bold + + def set_italic(self, italic=True): + """ + Set the Format italic property. + + Args: + italic: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.italic = italic + + def set_underline(self, underline=1): + """ + Set the Format underline property. + + Args: + underline: Default is 1, single underline. + + Returns: + Nothing. + + """ + self.underline = underline + + def set_font_strikeout(self, font_strikeout=True): + """ + Set the Format font_strikeout property. + + Args: + font_strikeout: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.font_strikeout = font_strikeout + + def set_font_script(self, font_script=1): + """ + Set the Format font_script property. + + Args: + font_script: Default is 1, superscript. + + Returns: + Nothing. + + """ + self.font_script = font_script + + def set_font_outline(self, font_outline=True): + """ + Set the Format font_outline property. + + Args: + font_outline: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.font_outline = font_outline + + def set_font_shadow(self, font_shadow=True): + """ + Set the Format font_shadow property. + + Args: + font_shadow: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.font_shadow = font_shadow + + def set_num_format(self, num_format): + """ + Set the Format num_format property such as '#,##0'. + + Args: + num_format: String representing the number format. No default. + + Returns: + Nothing. + + """ + self.num_format = num_format + + def set_locked(self, locked=True): + """ + Set the Format locked property. + + Args: + locked: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.locked = locked + + def set_hidden(self, hidden=True): + """ + Set the Format hidden property. + + Args: + hidden: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.hidden = hidden + + def set_align(self, alignment): + """ + Set the Format cell alignment. + + Args: + alignment: String representing alignment. No default. + + Returns: + Nothing. + """ + alignment = alignment.lower() + + # Set horizontal alignment properties. + if alignment == "left": + self.set_text_h_align(1) + if alignment == "centre": + self.set_text_h_align(2) + if alignment == "center": + self.set_text_h_align(2) + if alignment == "right": + self.set_text_h_align(3) + if alignment == "fill": + self.set_text_h_align(4) + if alignment == "justify": + self.set_text_h_align(5) + if alignment == "center_across": + self.set_text_h_align(6) + if alignment == "centre_across": + self.set_text_h_align(6) + if alignment == "distributed": + self.set_text_h_align(7) + if alignment == "justify_distributed": + self.set_text_h_align(7) + + if alignment == "justify_distributed": + self.just_distrib = 1 + + # Set vertical alignment properties. + if alignment == "top": + self.set_text_v_align(1) + if alignment == "vcentre": + self.set_text_v_align(2) + if alignment == "vcenter": + self.set_text_v_align(2) + if alignment == "bottom": + self.set_text_v_align(3) + if alignment == "vjustify": + self.set_text_v_align(4) + if alignment == "vdistributed": + self.set_text_v_align(5) + + def set_center_across(self, align_type=None): + # pylint: disable=unused-argument + """ + Set the Format center_across property. + + Returns: + Nothing. + + """ + self.set_text_h_align(6) + + def set_text_wrap(self, text_wrap=True): + """ + Set the Format text_wrap property. + + Args: + text_wrap: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.text_wrap = text_wrap + + def set_rotation(self, rotation): + """ + Set the Format rotation property. + + Args: + rotation: Rotation angle. No default. + + Returns: + Nothing. + + """ + rotation = int(rotation) + + # Map user angle to Excel angle. + if rotation == 270: + rotation = 255 + elif -90 <= rotation <= 90: + if rotation < 0: + rotation = -rotation + 90 + else: + warn("Rotation rotation outside range: -90 <= angle <= 90") + return + + self.rotation = rotation + + def set_indent(self, indent=1): + """ + Set the Format indent property. + + Args: + indent: Default is 1, first indentation level. + + Returns: + Nothing. + + """ + self.indent = indent + + def set_shrink(self, shrink=True): + """ + Set the Format shrink property. + + Args: + shrink: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.shrink = shrink + + def set_text_justlast(self, text_justlast=True): + """ + Set the Format text_justlast property. + + Args: + text_justlast: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.text_justlast = text_justlast + + def set_pattern(self, pattern=1): + """ + Set the Format pattern property. + + Args: + pattern: Default is 1, solid fill. + + Returns: + Nothing. + + """ + self.pattern = pattern + + def set_bg_color(self, bg_color): + """ + Set the Format bg_color property. + + Args: + bg_color: Background color. No default. + + Returns: + Nothing. + + """ + self.bg_color = self._get_color(bg_color) + + def set_fg_color(self, fg_color): + """ + Set the Format fg_color property. + + Args: + fg_color: Foreground color. No default. + + Returns: + Nothing. + + """ + self.fg_color = self._get_color(fg_color) + + # set_border(style) Set cells borders to the same style + def set_border(self, style=1): + """ + Set the Format bottom property. + + Args: + bottom: Default is 1, border type 1. + + Returns: + Nothing. + + """ + self.set_bottom(style) + self.set_top(style) + self.set_left(style) + self.set_right(style) + + # set_border_color(color) Set cells border to the same color + def set_border_color(self, color): + """ + Set the Format bottom property. + + Args: + color: Color string. No default. + + Returns: + Nothing. + + """ + self.set_bottom_color(color) + self.set_top_color(color) + self.set_left_color(color) + self.set_right_color(color) + + def set_bottom(self, bottom=1): + """ + Set the Format bottom property. + + Args: + bottom: Default is 1, border type 1. + + Returns: + Nothing. + + """ + self.bottom = bottom + + def set_bottom_color(self, bottom_color): + """ + Set the Format bottom_color property. + + Args: + bottom_color: Color string. No default. + + Returns: + Nothing. + + """ + self.bottom_color = self._get_color(bottom_color) + + def set_diag_type(self, diag_type=1): + """ + Set the Format diag_type property. + + Args: + diag_type: Default is 1, border type 1. + + Returns: + Nothing. + + """ + self.diag_type = diag_type + + def set_left(self, left=1): + """ + Set the Format left property. + + Args: + left: Default is 1, border type 1. + + Returns: + Nothing. + + """ + self.left = left + + def set_left_color(self, left_color): + """ + Set the Format left_color property. + + Args: + left_color: Color string. No default. + + Returns: + Nothing. + + """ + self.left_color = self._get_color(left_color) + + def set_right(self, right=1): + """ + Set the Format right property. + + Args: + right: Default is 1, border type 1. + + Returns: + Nothing. + + """ + self.right = right + + def set_right_color(self, right_color): + """ + Set the Format right_color property. + + Args: + right_color: Color string. No default. + + Returns: + Nothing. + + """ + self.right_color = self._get_color(right_color) + + def set_top(self, top=1): + """ + Set the Format top property. + + Args: + top: Default is 1, border type 1. + + Returns: + Nothing. + + """ + self.top = top + + def set_top_color(self, top_color): + """ + Set the Format top_color property. + + Args: + top_color: Color string. No default. + + Returns: + Nothing. + + """ + self.top_color = self._get_color(top_color) + + def set_diag_color(self, diag_color): + """ + Set the Format diag_color property. + + Args: + diag_color: Color string. No default. + + Returns: + Nothing. + + """ + self.diag_color = self._get_color(diag_color) + + def set_diag_border(self, diag_border=1): + """ + Set the Format diag_border property. + + Args: + diag_border: Default is 1, border type 1. + + Returns: + Nothing. + + """ + self.diag_border = diag_border + + def set_quote_prefix(self, quote_prefix=True): + """ + Set the Format quote prefix property. + + Args: + quote_prefix: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.quote_prefix = quote_prefix + + def set_checkbox(self, checkbox=True): + """ + Set the Format property to show a checkbox in a cell. + + This format property can be used with a cell that contains a boolean + value to display it as a checkbox. This property isn't required very + often and it is generally easier to create a checkbox using the + ``worksheet.insert_checkbox()`` method. + + Args: + checkbox: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.checkbox = checkbox + + ########################################################################### + # + # Internal Format properties. These aren't documented since they are + # either only used internally or else are unlikely to be set by the user. + # + ########################################################################### + + def set_has_font(self, has_font=True): + """ + Set the property to indicate the format has a font. + + Args: + has_font: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.has_font = has_font + + def set_has_fill(self, has_fill=True): + """ + Set the property to indicate the format has a fill. + + Args: + has_fill: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.has_fill = has_fill + + def set_font_index(self, font_index): + """ + Set the unique font index property. + + Args: + font_index: The unique font index. + + Returns: + Nothing. + + """ + self.font_index = font_index + + def set_xf_index(self, xf_index): + """ + Set the unique format index property. + + Args: + xf_index: The unique Excel format index. + + Returns: + Nothing. + + """ + self.xf_index = xf_index + + def set_dxf_index(self, dxf_index): + """ + Set the unique conditional format index property. + + Args: + dxf_index: The unique Excel conditional format index. + + Returns: + Nothing. + + """ + self.dxf_index = dxf_index + + def set_num_format_index(self, num_format_index): + """ + Set the number format_index property. + + Args: + num_format_index: The unique number format index. + + Returns: + Nothing. + + """ + self.num_format_index = num_format_index + + def set_text_h_align(self, text_h_align): + """ + Set the horizontal text alignment property. + + Args: + text_h_align: Horizontal text alignment. + + Returns: + Nothing. + + """ + self.text_h_align = text_h_align + + def set_text_v_align(self, text_v_align): + """ + Set the vertical text alignment property. + + Args: + text_h_align: Vertical text alignment. + + Returns: + Nothing. + + """ + self.text_v_align = text_v_align + + def set_reading_order(self, direction=0): + # Set the reading_order property. + """ + Set the reading order property. + + Args: + direction: Default is 0, left to right. + + Returns: + Nothing. + + """ + self.reading_order = direction + + def set_valign(self, align): + # Set vertical cell alignment. This is required by the constructor + # properties dict to differentiate between the vertical and horizontal + # properties. + """ + Set vertical cell alignment property. + + This is required by the constructor properties dict to differentiate + between the vertical and horizontal properties. + + Args: + align: Alignment property. + + Returns: + Nothing. + + """ + self.set_align(align) + + def set_font_family(self, font_family): + """ + Set the font family property. + + Args: + font_family: Font family number. + + Returns: + Nothing. + + """ + self.font_family = font_family + + def set_font_charset(self, font_charset): + """ + Set the font character set property. + + Args: + font_charset: The font character set number. + + Returns: + Nothing. + + """ + self.font_charset = font_charset + + def set_font_scheme(self, font_scheme): + """ + Set the font scheme property. + + Args: + font_scheme: The font scheme. + + Returns: + Nothing. + + """ + self.font_scheme = font_scheme + + def set_font_condense(self, font_condense): + """ + Set the font condense property. + + Args: + font_condense: The font condense property. + + Returns: + Nothing. + + """ + self.font_condense = font_condense + + def set_font_extend(self, font_extend): + """ + Set the font extend property. + + Args: + font_extend: The font extend property. + + Returns: + Nothing. + + """ + self.font_extend = font_extend + + def set_theme(self, theme): + """ + Set the theme property. + + Args: + theme: Format theme. + + Returns: + Nothing. + + """ + self.theme = theme + + def set_hyperlink(self, hyperlink=True): + """ + Set the properties for the hyperlink style. + + Args: + hyperlink: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.xf_id = 1 + self.set_underline(1) + self.set_theme(10) + self.hyperlink = hyperlink + + def set_color_indexed(self, color_index): + """ + Set the color index property. Some fundamental format properties use an + indexed color instead of a rbg or theme color. + + Args: + color_index: Generally 0 or 1. + + Returns: + Nothing. + + """ + self.color_indexed = color_index + + def set_font_only(self, font_only=True): + """ + Set property to indicate that the format is used for fonts only. + + Args: + font_only: Default is True, turns property on. + + Returns: + Nothing. + + """ + self.font_only = font_only + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _get_align_properties(self): + # pylint: disable=too-many-boolean-expressions + # Return properties for an Style xf <alignment> sub-element. + changed = 0 + align = [] + + # Check if any alignment options in the format have been changed. + if ( + self.text_h_align + or self.text_v_align + or self.indent + or self.rotation + or self.text_wrap + or self.shrink + or self.reading_order + ): + changed = 1 + else: + return changed, align + + # Indent is only allowed for some alignment properties. If it is + # defined for any other alignment or no alignment has been set then + # default to left alignment. + if ( + self.indent + and self.text_h_align != 1 + and self.text_h_align != 3 + and self.text_h_align != 7 + and self.text_v_align != 1 + and self.text_v_align != 3 + and self.text_v_align != 5 + ): + self.text_h_align = 1 + + # Check for properties that are mutually exclusive. + if self.text_wrap: + self.shrink = 0 + if self.text_h_align == 4: + self.shrink = 0 + if self.text_h_align == 5: + self.shrink = 0 + if self.text_h_align == 7: + self.shrink = 0 + if self.text_h_align != 7: + self.just_distrib = 0 + if self.indent: + self.just_distrib = 0 + + continuous = "centerContinuous" + + if self.text_h_align == 1: + align.append(("horizontal", "left")) + if self.text_h_align == 2: + align.append(("horizontal", "center")) + if self.text_h_align == 3: + align.append(("horizontal", "right")) + if self.text_h_align == 4: + align.append(("horizontal", "fill")) + if self.text_h_align == 5: + align.append(("horizontal", "justify")) + if self.text_h_align == 6: + align.append(("horizontal", continuous)) + if self.text_h_align == 7: + align.append(("horizontal", "distributed")) + + if self.just_distrib: + align.append(("justifyLastLine", 1)) + + # Property 'vertical' => 'bottom' is a default. It sets applyAlignment + # without an alignment sub-element. + if self.text_v_align == 1: + align.append(("vertical", "top")) + if self.text_v_align == 2: + align.append(("vertical", "center")) + if self.text_v_align == 4: + align.append(("vertical", "justify")) + if self.text_v_align == 5: + align.append(("vertical", "distributed")) + + if self.rotation: + align.append(("textRotation", self.rotation)) + if self.indent: + align.append(("indent", self.indent)) + + if self.text_wrap: + align.append(("wrapText", 1)) + if self.shrink: + align.append(("shrinkToFit", 1)) + + if self.reading_order == 1: + align.append(("readingOrder", 1)) + if self.reading_order == 2: + align.append(("readingOrder", 2)) + + return changed, align + + def _get_protection_properties(self): + # Return properties for an Excel XML <Protection> element. + attributes = [] + + if not self.locked: + attributes.append(("locked", 0)) + if self.hidden: + attributes.append(("hidden", 1)) + + return attributes + + def _get_format_key(self): + # Returns a unique hash key for a format. Used by Workbook. + if self._format_key is None: + self._format_key = ":".join( + str(x) + for x in ( + self._get_font_key(), + self._get_border_key(), + self._get_fill_key(), + self._get_alignment_key(), + self.num_format, + self.locked, + self.checkbox, + self.quote_prefix, + self.hidden, + ) + ) + + return self._format_key + + def _get_font_key(self): + # Returns a unique hash key for a font. Used by Workbook. + key = ":".join( + str(x) + for x in ( + self.bold, + self.font_color, + self.font_charset, + self.font_family, + self.font_outline, + self.font_script, + self.font_shadow, + self.font_strikeout, + self.font_name, + self.italic, + self.font_size, + self.underline, + self.theme, + ) + ) + + return key + + def _get_border_key(self): + # Returns a unique hash key for a border style. Used by Workbook. + key = ":".join( + str(x) + for x in ( + self.bottom, + self.bottom_color, + self.diag_border, + self.diag_color, + self.diag_type, + self.left, + self.left_color, + self.right, + self.right_color, + self.top, + self.top_color, + ) + ) + + return key + + def _get_fill_key(self): + # Returns a unique hash key for a fill style. Used by Workbook. + key = ":".join(str(x) for x in (self.pattern, self.bg_color, self.fg_color)) + + return key + + def _get_alignment_key(self): + # Returns a unique hash key for alignment formats. + + key = ":".join( + str(x) + for x in ( + self.text_h_align, + self.text_v_align, + self.indent, + self.rotation, + self.text_wrap, + self.shrink, + self.reading_order, + ) + ) + + return key + + def _get_xf_index(self): + # Returns the XF index number used by Excel to identify a format. + if self.xf_index is not None: + # Format already has an index number so return it. + return self.xf_index + + # Format doesn't have an index number so assign one. + key = self._get_format_key() + + if key in self.xf_format_indices: + # Format matches existing format with an index. + return self.xf_format_indices[key] + + # New format requiring an index. Note. +1 since Excel + # has an implicit "General" format at index 0. + index = 1 + len(self.xf_format_indices) + self.xf_format_indices[key] = index + self.xf_index = index + return index + + def _get_dxf_index(self): + # Returns the DXF index number used by Excel to identify a format. + if self.dxf_index is not None: + # Format already has an index number so return it. + return self.dxf_index + + # Format doesn't have an index number so assign one. + key = self._get_format_key() + + if key in self.dxf_format_indices: + # Format matches existing format with an index. + return self.dxf_format_indices[key] + + # New format requiring an index. + index = len(self.dxf_format_indices) + self.dxf_format_indices[key] = index + self.dxf_index = index + return index + + def _get_color(self, color): + # Used in conjunction with the set_xxx_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", + "automatic": "Automatic", + } + + return named_colors.get(color, color) + + ########################################################################### + # + # XML methods. + # + ########################################################################### diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/metadata.py b/.venv/lib/python3.12/site-packages/xlsxwriter/metadata.py new file mode 100644 index 00000000..284d65e4 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/metadata.py @@ -0,0 +1,266 @@ +############################################################################### +# +# Metadata - A class for writing the Excel XLSX Metadata file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from . import xmlwriter + + +class Metadata(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX Metadata file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + self.has_dynamic_functions = False + self.has_embedded_images = False + self.num_embedded_images = 0 + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + if self.num_embedded_images > 0: + self.has_embedded_images = True + + # Write the XML declaration. + self._xml_declaration() + + # Write the metadata element. + self._write_metadata() + + # Write the metadataTypes element. + self._write_metadata_types() + + # Write the futureMetadata elements. + if self.has_dynamic_functions: + self._write_cell_future_metadata() + if self.has_embedded_images: + self._write_value_future_metadata() + + # Write the cellMetadata element. + if self.has_dynamic_functions: + self._write_cell_metadata() + if self.has_embedded_images: + self._write_value_metadata() + + self._xml_end_tag("metadata") + + # Close the file. + self._xml_close() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_metadata(self): + # Write the <metadata> element. + xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" + schema = "http://schemas.microsoft.com/office/spreadsheetml" + + attributes = [("xmlns", xmlns)] + + if self.has_embedded_images: + attributes.append(("xmlns:xlrd", schema + "/2017/richdata")) + + if self.has_dynamic_functions: + attributes.append(("xmlns:xda", schema + "/2017/dynamicarray")) + + self._xml_start_tag("metadata", attributes) + + def _write_metadata_types(self): + # Write the <metadataTypes> element. + count = 0 + + if self.has_dynamic_functions: + count += 1 + if self.has_embedded_images: + count += 1 + + attributes = [("count", count)] + + self._xml_start_tag("metadataTypes", attributes) + + # Write the metadataType element. + if self.has_dynamic_functions: + self._write_cell_metadata_type() + if self.has_embedded_images: + self._write_value_metadata_type() + + self._xml_end_tag("metadataTypes") + + def _write_cell_metadata_type(self): + # Write the <metadataType> element. + attributes = [ + ("name", "XLDAPR"), + ("minSupportedVersion", 120000), + ("copy", 1), + ("pasteAll", 1), + ("pasteValues", 1), + ("merge", 1), + ("splitFirst", 1), + ("rowColShift", 1), + ("clearFormats", 1), + ("clearComments", 1), + ("assign", 1), + ("coerce", 1), + ("cellMeta", 1), + ] + + self._xml_empty_tag("metadataType", attributes) + + def _write_value_metadata_type(self): + # Write the <metadataType> element. + attributes = [ + ("name", "XLRICHVALUE"), + ("minSupportedVersion", 120000), + ("copy", 1), + ("pasteAll", 1), + ("pasteValues", 1), + ("merge", 1), + ("splitFirst", 1), + ("rowColShift", 1), + ("clearFormats", 1), + ("clearComments", 1), + ("assign", 1), + ("coerce", 1), + ] + + self._xml_empty_tag("metadataType", attributes) + + def _write_cell_future_metadata(self): + # Write the <futureMetadata> element. + attributes = [ + ("name", "XLDAPR"), + ("count", 1), + ] + + self._xml_start_tag("futureMetadata", attributes) + self._xml_start_tag("bk") + self._xml_start_tag("extLst") + self._write_cell_ext() + self._xml_end_tag("extLst") + self._xml_end_tag("bk") + self._xml_end_tag("futureMetadata") + + def _write_value_future_metadata(self): + # Write the <futureMetadata> element. + attributes = [ + ("name", "XLRICHVALUE"), + ("count", self.num_embedded_images), + ] + + self._xml_start_tag("futureMetadata", attributes) + + for index in range(self.num_embedded_images): + self._xml_start_tag("bk") + self._xml_start_tag("extLst") + self._write_value_ext(index) + self._xml_end_tag("extLst") + self._xml_end_tag("bk") + + self._xml_end_tag("futureMetadata") + + def _write_cell_ext(self): + # Write the <ext> element. + attributes = [("uri", "{bdbb8cdc-fa1e-496e-a857-3c3f30c029c3}")] + + self._xml_start_tag("ext", attributes) + + # Write the xda:dynamicArrayProperties element. + self._write_xda_dynamic_array_properties() + + self._xml_end_tag("ext") + + def _write_xda_dynamic_array_properties(self): + # Write the <xda:dynamicArrayProperties> element. + attributes = [ + ("fDynamic", 1), + ("fCollapsed", 0), + ] + + self._xml_empty_tag("xda:dynamicArrayProperties", attributes) + + def _write_value_ext(self, index): + # Write the <ext> element. + attributes = [("uri", "{3e2802c4-a4d2-4d8b-9148-e3be6c30e623}")] + + self._xml_start_tag("ext", attributes) + + # Write the xlrd:rvb element. + self._write_xlrd_rvb(index) + + self._xml_end_tag("ext") + + def _write_xlrd_rvb(self, index): + # Write the <xlrd:rvb> element. + attributes = [("i", index)] + + self._xml_empty_tag("xlrd:rvb", attributes) + + def _write_cell_metadata(self): + # Write the <cellMetadata> element. + attributes = [("count", 1)] + + self._xml_start_tag("cellMetadata", attributes) + self._xml_start_tag("bk") + + # Write the rc element. + self._write_rc(1, 0) + + self._xml_end_tag("bk") + self._xml_end_tag("cellMetadata") + + def _write_value_metadata(self): + # Write the <valueMetadata> element. + count = self.num_embedded_images + rc_type = 1 + + if self.has_dynamic_functions: + rc_type = 2 + + attributes = [("count", count)] + + self._xml_start_tag("valueMetadata", attributes) + + # Write the rc elements. + for index in range(self.num_embedded_images): + self._xml_start_tag("bk") + self._write_rc(rc_type, index) + self._xml_end_tag("bk") + + self._xml_end_tag("valueMetadata") + + def _write_rc(self, rc_type, index): + # Write the <rc> element. + attributes = [ + ("t", rc_type), + ("v", index), + ] + + self._xml_empty_tag("rc", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/packager.py b/.venv/lib/python3.12/site-packages/xlsxwriter/packager.py new file mode 100644 index 00000000..17587f0a --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/packager.py @@ -0,0 +1,880 @@ +############################################################################### +# +# Packager - A class for writing the Excel XLSX Worksheet file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Standard packages. +import os +import stat +import tempfile +from io import BytesIO, StringIO +from shutil import copy + +# Package imports. +from .app import App +from .comments import Comments +from .contenttypes import ContentTypes +from .core import Core +from .custom import Custom +from .exceptions import EmptyChartSeries +from .feature_property_bag import FeaturePropertyBag +from .metadata import Metadata +from .relationships import Relationships +from .rich_value import RichValue +from .rich_value_rel import RichValueRel +from .rich_value_structure import RichValueStructure +from .rich_value_types import RichValueTypes +from .sharedstrings import SharedStrings +from .styles import Styles +from .table import Table +from .theme import Theme +from .vml import Vml + + +class Packager: + """ + A class for writing the Excel XLSX Packager file. + + This module is used in conjunction with XlsxWriter to create an + Excel XLSX container file. + + From Wikipedia: The Open Packaging Conventions (OPC) is a + container-file technology initially created by Microsoft to store + a combination of XML and non-XML files that together form a single + entity such as an Open XML Paper Specification (OpenXPS) + document. http://en.wikipedia.org/wiki/Open_Packaging_Conventions. + + At its simplest an Excel XLSX file contains the following elements:: + + ____ [Content_Types].xml + | + |____ docProps + | |____ app.xml + | |____ core.xml + | + |____ xl + | |____ workbook.xml + | |____ worksheets + | | |____ sheet1.xml + | | + | |____ styles.xml + | | + | |____ theme + | | |____ theme1.xml + | | + | |_____rels + | |____ workbook.xml.rels + | + |_____rels + |____ .rels + + The Packager class coordinates the classes that represent the + elements of the package and writes them into the XLSX file. + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + self.tmpdir = "" + self.in_memory = False + self.workbook = None + self.worksheet_count = 0 + self.chartsheet_count = 0 + self.chart_count = 0 + self.drawing_count = 0 + self.table_count = 0 + self.num_vml_files = 0 + self.num_comment_files = 0 + self.named_ranges = [] + self.filenames = [] + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _set_tmpdir(self, tmpdir): + # Set an optional user defined temp directory. + self.tmpdir = tmpdir + + def _set_in_memory(self, in_memory): + # Set the optional 'in_memory' mode. + self.in_memory = in_memory + + def _add_workbook(self, workbook): + # Add the Excel::Writer::XLSX::Workbook object to the package. + self.workbook = workbook + self.chart_count = len(workbook.charts) + self.drawing_count = len(workbook.drawings) + self.num_vml_files = workbook.num_vml_files + self.num_comment_files = workbook.num_comment_files + self.named_ranges = workbook.named_ranges + + for worksheet in self.workbook.worksheets(): + if worksheet.is_chartsheet: + self.chartsheet_count += 1 + else: + self.worksheet_count += 1 + + def _create_package(self): + # Write the xml files that make up the XLSX OPC package. + self._write_content_types_file() + self._write_root_rels_file() + self._write_workbook_rels_file() + self._write_worksheet_files() + self._write_chartsheet_files() + self._write_workbook_file() + self._write_chart_files() + self._write_drawing_files() + self._write_vml_files() + self._write_comment_files() + self._write_table_files() + self._write_shared_strings_file() + self._write_styles_file() + self._write_custom_file() + self._write_theme_file() + self._write_worksheet_rels_files() + self._write_chartsheet_rels_files() + self._write_drawing_rels_files() + self._write_rich_value_rels_files() + self._add_image_files() + self._add_vba_project() + self._add_vba_project_signature() + self._write_vba_project_rels_file() + self._write_core_file() + self._write_app_file() + self._write_metadata_file() + self._write_feature_bag_property() + self._write_rich_value_files() + + return self.filenames + + def _filename(self, xml_filename): + # Create a temp filename to write the XML data to and store the Excel + # filename to use as the name in the Zip container. + if self.in_memory: + os_filename = StringIO() + else: + (fd, os_filename) = tempfile.mkstemp(dir=self.tmpdir) + os.close(fd) + + self.filenames.append((os_filename, xml_filename, False)) + + return os_filename + + def _write_workbook_file(self): + # Write the workbook.xml file. + workbook = self.workbook + + workbook._set_xml_writer(self._filename("xl/workbook.xml")) + workbook._assemble_xml_file() + + def _write_worksheet_files(self): + # Write the worksheet files. + index = 1 + for worksheet in self.workbook.worksheets(): + if worksheet.is_chartsheet: + continue + + if worksheet.constant_memory: + worksheet._opt_reopen() + worksheet._write_single_row() + + worksheet._set_xml_writer( + self._filename("xl/worksheets/sheet" + str(index) + ".xml") + ) + worksheet._assemble_xml_file() + index += 1 + + def _write_chartsheet_files(self): + # Write the chartsheet files. + index = 1 + for worksheet in self.workbook.worksheets(): + if not worksheet.is_chartsheet: + continue + + worksheet._set_xml_writer( + self._filename("xl/chartsheets/sheet" + str(index) + ".xml") + ) + worksheet._assemble_xml_file() + index += 1 + + def _write_chart_files(self): + # Write the chart files. + if not self.workbook.charts: + return + + index = 1 + for chart in self.workbook.charts: + # Check that the chart has at least one data series. + if not chart.series: + raise EmptyChartSeries( + f"Chart{index} must contain at least one " + f"data series. See chart.add_series()." + ) + + chart._set_xml_writer( + self._filename("xl/charts/chart" + str(index) + ".xml") + ) + chart._assemble_xml_file() + index += 1 + + def _write_drawing_files(self): + # Write the drawing files. + if not self.drawing_count: + return + + index = 1 + for drawing in self.workbook.drawings: + drawing._set_xml_writer( + self._filename("xl/drawings/drawing" + str(index) + ".xml") + ) + drawing._assemble_xml_file() + index += 1 + + def _write_vml_files(self): + # Write the comment VML files. + index = 1 + for worksheet in self.workbook.worksheets(): + if not worksheet.has_vml and not worksheet.has_header_vml: + continue + if worksheet.has_vml: + vml = Vml() + vml._set_xml_writer( + self._filename("xl/drawings/vmlDrawing" + str(index) + ".vml") + ) + vml._assemble_xml_file( + worksheet.vml_data_id, + worksheet.vml_shape_id, + worksheet.comments_list, + worksheet.buttons_list, + ) + index += 1 + + if worksheet.has_header_vml: + vml = Vml() + + vml._set_xml_writer( + self._filename("xl/drawings/vmlDrawing" + str(index) + ".vml") + ) + vml._assemble_xml_file( + worksheet.vml_header_id, + worksheet.vml_header_id * 1024, + None, + None, + worksheet.header_images_list, + ) + + self._write_vml_drawing_rels_file(worksheet, index) + index += 1 + + def _write_comment_files(self): + # Write the comment files. + index = 1 + for worksheet in self.workbook.worksheets(): + if not worksheet.has_comments: + continue + + comment = Comments() + comment._set_xml_writer(self._filename("xl/comments" + str(index) + ".xml")) + comment._assemble_xml_file(worksheet.comments_list) + index += 1 + + def _write_shared_strings_file(self): + # Write the sharedStrings.xml file. + sst = SharedStrings() + sst.string_table = self.workbook.str_table + + if not self.workbook.str_table.count: + return + + sst._set_xml_writer(self._filename("xl/sharedStrings.xml")) + sst._assemble_xml_file() + + def _write_app_file(self): + # Write the app.xml file. + properties = self.workbook.doc_properties + app = App() + + # Add the Worksheet parts. + worksheet_count = 0 + for worksheet in self.workbook.worksheets(): + if worksheet.is_chartsheet: + continue + + # Don't write/count veryHidden sheets. + if worksheet.hidden != 2: + app._add_part_name(worksheet.name) + worksheet_count += 1 + + # Add the Worksheet heading pairs. + app._add_heading_pair(["Worksheets", worksheet_count]) + + # Add the Chartsheet parts. + for worksheet in self.workbook.worksheets(): + if not worksheet.is_chartsheet: + continue + app._add_part_name(worksheet.name) + + # Add the Chartsheet heading pairs. + app._add_heading_pair(["Charts", self.chartsheet_count]) + + # Add the Named Range heading pairs. + if self.named_ranges: + app._add_heading_pair(["Named Ranges", len(self.named_ranges)]) + + # Add the Named Ranges parts. + for named_range in self.named_ranges: + app._add_part_name(named_range) + + app._set_properties(properties) + app.doc_security = self.workbook.read_only + + app._set_xml_writer(self._filename("docProps/app.xml")) + app._assemble_xml_file() + + def _write_core_file(self): + # Write the core.xml file. + properties = self.workbook.doc_properties + core = Core() + + core._set_properties(properties) + core._set_xml_writer(self._filename("docProps/core.xml")) + core._assemble_xml_file() + + def _write_metadata_file(self): + # Write the metadata.xml file. + if not self.workbook.has_metadata: + return + + metadata = Metadata() + metadata.has_dynamic_functions = self.workbook.has_dynamic_functions + metadata.num_embedded_images = len(self.workbook.embedded_images.images) + + metadata._set_xml_writer(self._filename("xl/metadata.xml")) + metadata._assemble_xml_file() + + def _write_feature_bag_property(self): + # Write the featurePropertyBag.xml file. + feature_property_bags = self.workbook._has_feature_property_bags() + if not feature_property_bags: + return + + property_bag = FeaturePropertyBag() + property_bag.feature_property_bags = feature_property_bags + + property_bag._set_xml_writer( + self._filename("xl/featurePropertyBag/featurePropertyBag.xml") + ) + property_bag._assemble_xml_file() + + def _write_rich_value_files(self): + + if not self.workbook.embedded_images.has_images(): + return + + self._write_rich_value() + self._write_rich_value_types() + self._write_rich_value_structure() + self._write_rich_value_rel() + + def _write_rich_value(self): + # Write the rdrichvalue.xml file. + filename = self._filename("xl/richData/rdrichvalue.xml") + xml_file = RichValue() + xml_file.embedded_images = self.workbook.embedded_images.images + xml_file._set_xml_writer(filename) + xml_file._assemble_xml_file() + + def _write_rich_value_types(self): + # Write the rdRichValueTypes.xml file. + filename = self._filename("xl/richData/rdRichValueTypes.xml") + xml_file = RichValueTypes() + xml_file._set_xml_writer(filename) + xml_file._assemble_xml_file() + + def _write_rich_value_structure(self): + # Write the rdrichvaluestructure.xml file. + filename = self._filename("xl/richData/rdrichvaluestructure.xml") + xml_file = RichValueStructure() + xml_file.has_embedded_descriptions = self.workbook.has_embedded_descriptions + xml_file._set_xml_writer(filename) + xml_file._assemble_xml_file() + + def _write_rich_value_rel(self): + # Write the richValueRel.xml file. + filename = self._filename("xl/richData/richValueRel.xml") + xml_file = RichValueRel() + xml_file.num_embedded_images = len(self.workbook.embedded_images.images) + xml_file._set_xml_writer(filename) + xml_file._assemble_xml_file() + + def _write_custom_file(self): + # Write the custom.xml file. + properties = self.workbook.custom_properties + custom = Custom() + + if not properties: + return + + custom._set_properties(properties) + custom._set_xml_writer(self._filename("docProps/custom.xml")) + custom._assemble_xml_file() + + def _write_content_types_file(self): + # Write the ContentTypes.xml file. + content = ContentTypes() + content._add_image_types(self.workbook.image_types) + + self._get_table_count() + + worksheet_index = 1 + chartsheet_index = 1 + for worksheet in self.workbook.worksheets(): + if worksheet.is_chartsheet: + content._add_chartsheet_name("sheet" + str(chartsheet_index)) + chartsheet_index += 1 + else: + content._add_worksheet_name("sheet" + str(worksheet_index)) + worksheet_index += 1 + + for i in range(1, self.chart_count + 1): + content._add_chart_name("chart" + str(i)) + + for i in range(1, self.drawing_count + 1): + content._add_drawing_name("drawing" + str(i)) + + if self.num_vml_files: + content._add_vml_name() + + for i in range(1, self.table_count + 1): + content._add_table_name("table" + str(i)) + + for i in range(1, self.num_comment_files + 1): + content._add_comment_name("comments" + str(i)) + + # Add the sharedString rel if there is string data in the workbook. + if self.workbook.str_table.count: + content._add_shared_strings() + + # Add vbaProject (and optionally vbaProjectSignature) if present. + if self.workbook.vba_project: + content._add_vba_project() + if self.workbook.vba_project_signature: + content._add_vba_project_signature() + + # Add the custom properties if present. + if self.workbook.custom_properties: + content._add_custom_properties() + + # Add the metadata file if present. + if self.workbook.has_metadata: + content._add_metadata() + + # Add the metadata file if present. + if self.workbook._has_feature_property_bags(): + content._add_feature_bag_property() + + # Add the RichValue file if present. + if self.workbook.embedded_images.has_images(): + content._add_rich_value() + + content._set_xml_writer(self._filename("[Content_Types].xml")) + content._assemble_xml_file() + + def _write_styles_file(self): + # Write the style xml file. + xf_formats = self.workbook.xf_formats + palette = self.workbook.palette + font_count = self.workbook.font_count + num_formats = self.workbook.num_formats + border_count = self.workbook.border_count + fill_count = self.workbook.fill_count + custom_colors = self.workbook.custom_colors + dxf_formats = self.workbook.dxf_formats + has_comments = self.workbook.has_comments + + styles = Styles() + styles._set_style_properties( + [ + xf_formats, + palette, + font_count, + num_formats, + border_count, + fill_count, + custom_colors, + dxf_formats, + has_comments, + ] + ) + + styles._set_xml_writer(self._filename("xl/styles.xml")) + styles._assemble_xml_file() + + def _write_theme_file(self): + # Write the theme xml file. + theme = Theme() + + theme._set_xml_writer(self._filename("xl/theme/theme1.xml")) + theme._assemble_xml_file() + + def _write_table_files(self): + # Write the table files. + index = 1 + for worksheet in self.workbook.worksheets(): + table_props = worksheet.tables + + if not table_props: + continue + + for table_props in table_props: + table = Table() + table._set_xml_writer( + self._filename("xl/tables/table" + str(index) + ".xml") + ) + table._set_properties(table_props) + table._assemble_xml_file() + index += 1 + + def _get_table_count(self): + # Count the table files. Required for the [Content_Types] file. + for worksheet in self.workbook.worksheets(): + for _ in worksheet.tables: + self.table_count += 1 + + def _write_root_rels_file(self): + # Write the _rels/.rels xml file. + rels = Relationships() + + rels._add_document_relationship("/officeDocument", "xl/workbook.xml") + + rels._add_package_relationship("/metadata/core-properties", "docProps/core.xml") + + rels._add_document_relationship("/extended-properties", "docProps/app.xml") + + if self.workbook.custom_properties: + rels._add_document_relationship("/custom-properties", "docProps/custom.xml") + + rels._set_xml_writer(self._filename("_rels/.rels")) + + rels._assemble_xml_file() + + def _write_workbook_rels_file(self): + # Write the _rels/.rels xml file. + rels = Relationships() + + worksheet_index = 1 + chartsheet_index = 1 + + for worksheet in self.workbook.worksheets(): + if worksheet.is_chartsheet: + rels._add_document_relationship( + "/chartsheet", "chartsheets/sheet" + str(chartsheet_index) + ".xml" + ) + chartsheet_index += 1 + else: + rels._add_document_relationship( + "/worksheet", "worksheets/sheet" + str(worksheet_index) + ".xml" + ) + worksheet_index += 1 + + rels._add_document_relationship("/theme", "theme/theme1.xml") + rels._add_document_relationship("/styles", "styles.xml") + + # Add the sharedString rel if there is string data in the workbook. + if self.workbook.str_table.count: + rels._add_document_relationship("/sharedStrings", "sharedStrings.xml") + + # Add vbaProject if present. + if self.workbook.vba_project: + rels._add_ms_package_relationship("/vbaProject", "vbaProject.bin") + + # Add the metadata file if required. + if self.workbook.has_metadata: + rels._add_document_relationship("/sheetMetadata", "metadata.xml") + + # Add the RichValue files if present. + if self.workbook.embedded_images.has_images(): + rels._add_rich_value_relationship() + + # Add the checkbox/FeaturePropertyBag file if present. + if self.workbook._has_feature_property_bags(): + rels._add_feature_bag_relationship() + + rels._set_xml_writer(self._filename("xl/_rels/workbook.xml.rels")) + rels._assemble_xml_file() + + def _write_worksheet_rels_files(self): + # Write data such as hyperlinks or drawings. + index = 0 + for worksheet in self.workbook.worksheets(): + if worksheet.is_chartsheet: + continue + + index += 1 + + external_links = ( + worksheet.external_hyper_links + + worksheet.external_drawing_links + + worksheet.external_vml_links + + worksheet.external_background_links + + worksheet.external_table_links + + worksheet.external_comment_links + ) + + if not external_links: + continue + + # Create the worksheet .rels dirs. + rels = Relationships() + + for link_data in external_links: + rels._add_document_relationship(*link_data) + + # Create .rels file such as /xl/worksheets/_rels/sheet1.xml.rels. + rels._set_xml_writer( + self._filename("xl/worksheets/_rels/sheet" + str(index) + ".xml.rels") + ) + rels._assemble_xml_file() + + def _write_chartsheet_rels_files(self): + # Write the chartsheet .rels files for links to drawing files. + index = 0 + for worksheet in self.workbook.worksheets(): + if not worksheet.is_chartsheet: + continue + + index += 1 + + external_links = ( + worksheet.external_drawing_links + worksheet.external_vml_links + ) + + if not external_links: + continue + + # Create the chartsheet .rels xlsx_dir. + rels = Relationships() + + for link_data in external_links: + rels._add_document_relationship(*link_data) + + # Create .rels file such as /xl/chartsheets/_rels/sheet1.xml.rels. + rels._set_xml_writer( + self._filename("xl/chartsheets/_rels/sheet" + str(index) + ".xml.rels") + ) + rels._assemble_xml_file() + + def _write_drawing_rels_files(self): + # Write the drawing .rels files for worksheets with charts or drawings. + index = 0 + for worksheet in self.workbook.worksheets(): + if worksheet.drawing: + index += 1 + + if not worksheet.drawing_links: + continue + + # Create the drawing .rels xlsx_dir. + rels = Relationships() + + for drawing_data in worksheet.drawing_links: + rels._add_document_relationship(*drawing_data) + + # Create .rels file such as /xl/drawings/_rels/sheet1.xml.rels. + rels._set_xml_writer( + self._filename("xl/drawings/_rels/drawing" + str(index) + ".xml.rels") + ) + rels._assemble_xml_file() + + def _write_vml_drawing_rels_file(self, worksheet, index): + # Write the vmlDdrawing .rels files for worksheets with images in + # headers or footers. + + # Create the drawing .rels dir. + rels = Relationships() + + for drawing_data in worksheet.vml_drawing_links: + rels._add_document_relationship(*drawing_data) + + # Create .rels file such as /xl/drawings/_rels/vmlDrawing1.vml.rels. + rels._set_xml_writer( + self._filename("xl/drawings/_rels/vmlDrawing" + str(index) + ".vml.rels") + ) + rels._assemble_xml_file() + + def _write_vba_project_rels_file(self): + # Write the vbaProject.rels xml file if signed macros exist. + vba_project_signature = self.workbook.vba_project_signature + + if not vba_project_signature: + return + + # Create the vbaProject .rels dir. + rels = Relationships() + + rels._add_ms_package_relationship( + "/vbaProjectSignature", "vbaProjectSignature.bin" + ) + + rels._set_xml_writer(self._filename("xl/_rels/vbaProject.bin.rels")) + rels._assemble_xml_file() + + def _write_rich_value_rels_files(self): + # Write the richValueRel.xml.rels for embedded images. + if not self.workbook.embedded_images.has_images(): + return + + # Create the worksheet .rels dirs. + rels = Relationships() + + index = 1 + for image_data in self.workbook.embedded_images.images: + file_type = image_data[1] + image_file = f"../media/image{index}.{file_type}" + rels._add_document_relationship("/image", image_file) + index += 1 + + # Create .rels file such as /xl/worksheets/_rels/sheet1.xml.rels. + rels._set_xml_writer(self._filename("/xl/richData/_rels/richValueRel.xml.rels")) + + rels._assemble_xml_file() + + def _add_image_files(self): + # pylint: disable=consider-using-with + # Write the /xl/media/image?.xml files. + workbook = self.workbook + index = 1 + + images = workbook.embedded_images.images + workbook.images + + for image in images: + filename = image[0] + ext = "." + image[1] + image_data = image[2] + + xml_image_name = "xl/media/image" + str(index) + ext + + if not self.in_memory: + # In file mode we just write or copy the image file. + os_filename = self._filename(xml_image_name) + + if image_data: + # The data is in a byte stream. Write it to the target. + os_file = open(os_filename, mode="wb") + os_file.write(image_data.getvalue()) + os_file.close() + else: + copy(filename, os_filename) + + # Allow copies of Windows read-only images to be deleted. + try: + os.chmod( + os_filename, os.stat(os_filename).st_mode | stat.S_IWRITE + ) + except OSError: + pass + else: + # For in-memory mode we read the image into a stream. + if image_data: + # The data is already in a byte stream. + os_filename = image_data + else: + image_file = open(filename, mode="rb") + image_data = image_file.read() + os_filename = BytesIO(image_data) + image_file.close() + + self.filenames.append((os_filename, xml_image_name, True)) + + index += 1 + + def _add_vba_project_signature(self): + # pylint: disable=consider-using-with + # Copy in a vbaProjectSignature.bin file. + vba_project_signature = self.workbook.vba_project_signature + vba_project_signature_is_stream = self.workbook.vba_project_signature_is_stream + + if not vba_project_signature: + return + + xml_vba_signature_name = "xl/vbaProjectSignature.bin" + + if not self.in_memory: + # In file mode we just write or copy the VBA project signature file. + os_filename = self._filename(xml_vba_signature_name) + + if vba_project_signature_is_stream: + # The data is in a byte stream. Write it to the target. + os_file = open(os_filename, mode="wb") + os_file.write(vba_project_signature.getvalue()) + os_file.close() + else: + copy(vba_project_signature, os_filename) + + else: + # For in-memory mode we read the vba into a stream. + if vba_project_signature_is_stream: + # The data is already in a byte stream. + os_filename = vba_project_signature + else: + vba_file = open(vba_project_signature, mode="rb") + vba_data = vba_file.read() + os_filename = BytesIO(vba_data) + vba_file.close() + + self.filenames.append((os_filename, xml_vba_signature_name, True)) + + def _add_vba_project(self): + # pylint: disable=consider-using-with + # Copy in a vbaProject.bin file. + vba_project = self.workbook.vba_project + vba_project_is_stream = self.workbook.vba_project_is_stream + + if not vba_project: + return + + xml_vba_name = "xl/vbaProject.bin" + + if not self.in_memory: + # In file mode we just write or copy the VBA file. + os_filename = self._filename(xml_vba_name) + + if vba_project_is_stream: + # The data is in a byte stream. Write it to the target. + os_file = open(os_filename, mode="wb") + os_file.write(vba_project.getvalue()) + os_file.close() + else: + copy(vba_project, os_filename) + + else: + # For in-memory mode we read the vba into a stream. + if vba_project_is_stream: + # The data is already in a byte stream. + os_filename = vba_project + else: + vba_file = open(vba_project, mode="rb") + vba_data = vba_file.read() + os_filename = BytesIO(vba_data) + vba_file.close() + + self.filenames.append((os_filename, xml_vba_name, True)) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/relationships.py b/.venv/lib/python3.12/site-packages/xlsxwriter/relationships.py new file mode 100644 index 00000000..9829c5fe --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/relationships.py @@ -0,0 +1,143 @@ +############################################################################### +# +# Relationships - A class for writing the Excel XLSX Worksheet file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Package imports. +from . import xmlwriter + +# Long namespace strings used in the class. +SCHEMA_ROOT = "http://schemas.openxmlformats.org" +PACKAGE_SCHEMA = SCHEMA_ROOT + "/package/2006/relationships" +DOCUMENT_SCHEMA = SCHEMA_ROOT + "/officeDocument/2006/relationships" + + +class Relationships(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX Relationships file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + self.relationships = [] + self.id = 1 + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + self._write_relationships() + + # Close the file. + self._xml_close() + + def _add_document_relationship(self, rel_type, target, target_mode=None): + # Add container relationship to XLSX .rels xml files. + rel_type = DOCUMENT_SCHEMA + rel_type + + self.relationships.append((rel_type, target, target_mode)) + + def _add_package_relationship(self, rel_type, target): + # Add container relationship to XLSX .rels xml files. + rel_type = PACKAGE_SCHEMA + rel_type + + self.relationships.append((rel_type, target, None)) + + def _add_ms_package_relationship(self, rel_type, target): + # Add container relationship to XLSX .rels xml files. Uses MS schema. + schema = "http://schemas.microsoft.com/office/2006/relationships" + rel_type = schema + rel_type + + self.relationships.append((rel_type, target, None)) + + def _add_rich_value_relationship(self): + # Add RichValue relationship to XLSX .rels xml files. + schema = "http://schemas.microsoft.com/office/2022/10/relationships/" + rel_type = schema + "richValueRel" + target = "richData/richValueRel.xml" + self.relationships.append((rel_type, target, None)) + + schema = "http://schemas.microsoft.com/office/2017/06/relationships/" + rel_type = schema + "rdRichValue" + target = "richData/rdrichvalue.xml" + self.relationships.append((rel_type, target, None)) + + rel_type = schema + "rdRichValueStructure" + target = "richData/rdrichvaluestructure.xml" + self.relationships.append((rel_type, target, None)) + + rel_type = schema + "rdRichValueTypes" + target = "richData/rdRichValueTypes.xml" + self.relationships.append((rel_type, target, None)) + + def _add_feature_bag_relationship(self): + # Add FeaturePropertyBag relationship to XLSX .rels xml files. + schema = "http://schemas.microsoft.com/office/2022/11/relationships/" + rel_type = schema + "FeaturePropertyBag" + target = "featurePropertyBag/featurePropertyBag.xml" + self.relationships.append((rel_type, target, None)) + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_relationships(self): + # Write the <Relationships> element. + attributes = [ + ( + "xmlns", + PACKAGE_SCHEMA, + ) + ] + + self._xml_start_tag("Relationships", attributes) + + for relationship in self.relationships: + self._write_relationship(relationship) + + self._xml_end_tag("Relationships") + + def _write_relationship(self, relationship): + # Write the <Relationship> element. + rel_type, target, target_mode = relationship + + attributes = [ + ("Id", "rId" + str(self.id)), + ("Type", rel_type), + ("Target", target), + ] + + self.id += 1 + + if target_mode: + attributes.append(("TargetMode", target_mode)) + + self._xml_empty_tag("Relationship", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/rich_value.py b/.venv/lib/python3.12/site-packages/xlsxwriter/rich_value.py new file mode 100644 index 00000000..6a53f0c6 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/rich_value.py @@ -0,0 +1,97 @@ +############################################################################### +# +# RichValue - A class for writing the Excel XLSX rdrichvalue.xml file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Package imports. +from . import xmlwriter + + +class RichValue(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX rdrichvalue.xml file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + self.embedded_images = [] + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + # Write the rvData element. + self._write_rv_data() + + self._xml_end_tag("rvData") + + # Close the file. + self._xml_close() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + def _write_rv_data(self): + # Write the <rvData> element. + xmlns = "http://schemas.microsoft.com/office/spreadsheetml/2017/richdata" + + attributes = [ + ("xmlns", xmlns), + ("count", len(self.embedded_images)), + ] + + self._xml_start_tag("rvData", attributes) + + for index, image_data in enumerate(self.embedded_images): + # Write the rv element. + self._write_rv(index, image_data[3], image_data[4]) + + def _write_rv(self, index, description, decorative): + # Write the <rv> element. + attributes = [("s", 0)] + value = 5 + + if decorative: + value = 6 + + self._xml_start_tag("rv", attributes) + + # Write the v elements. + self._write_v(index) + self._write_v(value) + + if description: + self._write_v(description) + + self._xml_end_tag("rv") + + def _write_v(self, data): + # Write the <v> element. + self._xml_data_element("v", data) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/rich_value_rel.py b/.venv/lib/python3.12/site-packages/xlsxwriter/rich_value_rel.py new file mode 100644 index 00000000..c8d85932 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/rich_value_rel.py @@ -0,0 +1,82 @@ +############################################################################### +# +# RichValueRel - A class for writing the Excel XLSX richValueRel.xml file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Package imports. +from . import xmlwriter + + +class RichValueRel(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX richValueRel.xml file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + self.num_embedded_images = 0 + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + # Write the richValueRels element. + self._write_rich_value_rels() + + self._xml_end_tag("richValueRels") + + # Close the file. + self._xml_close() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + def _write_rich_value_rels(self): + # Write the <richValueRels> element. + xmlns = "http://schemas.microsoft.com/office/spreadsheetml/2022/richvaluerel" + xmlns_r = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + + attributes = [ + ("xmlns", xmlns), + ("xmlns:r", xmlns_r), + ] + + self._xml_start_tag("richValueRels", attributes) + + # Write the rel elements. + for index in range(self.num_embedded_images): + self._write_rel(index + 1) + + def _write_rel(self, index): + # Write the <rel> element. + r_id = f"rId{index}" + attributes = [("r:id", r_id)] + + self._xml_empty_tag("rel", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/rich_value_structure.py b/.venv/lib/python3.12/site-packages/xlsxwriter/rich_value_structure.py new file mode 100644 index 00000000..33e0f021 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/rich_value_structure.py @@ -0,0 +1,99 @@ +############################################################################### +# +# RichValueStructure - A class for writing the Excel XLSX rdrichvaluestructure.xml file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Package imports. +from . import xmlwriter + + +class RichValueStructure(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX rdrichvaluestructure.xml file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + self.has_embedded_descriptions = False + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + # Write the rvStructures element. + self._write_rv_structures() + + self._xml_end_tag("rvStructures") + + # Close the file. + self._xml_close() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + def _write_rv_structures(self): + # Write the <rvStructures> element. + xmlns = "http://schemas.microsoft.com/office/spreadsheetml/2017/richdata" + count = "1" + + attributes = [ + ("xmlns", xmlns), + ("count", count), + ] + + self._xml_start_tag("rvStructures", attributes) + + # Write the s element. + self._write_s() + + def _write_s(self): + # Write the <s> element. + t = "_localImage" + attributes = [("t", t)] + + self._xml_start_tag("s", attributes) + + # Write the k elements. + self._write_k("_rvRel:LocalImageIdentifier", "i") + self._write_k("CalcOrigin", "i") + + if self.has_embedded_descriptions: + self._write_k("Text", "s") + + self._xml_end_tag("s") + + def _write_k(self, name, k_type): + # Write the <k> element. + attributes = [ + ("n", name), + ("t", k_type), + ] + + self._xml_empty_tag("k", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/rich_value_types.py b/.venv/lib/python3.12/site-packages/xlsxwriter/rich_value_types.py new file mode 100644 index 00000000..39c288b0 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/rich_value_types.py @@ -0,0 +1,111 @@ +############################################################################### +# +# RichValueTypes - A class for writing the Excel XLSX rdRichValueTypes.xml file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Package imports. +from . import xmlwriter + + +class RichValueTypes(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX rdRichValueTypes.xml file. + + + """ + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + # Write the rvTypesInfo element. + self._write_rv_types_info() + + # Write the global element. + self._write_global() + + self._xml_end_tag("rvTypesInfo") + + # Close the file. + self._xml_close() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_rv_types_info(self): + # Write the <rvTypesInfo> element. + xmlns = "http://schemas.microsoft.com/office/spreadsheetml/2017/richdata2" + xmlns_x = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" + xmlns_mc = "http://schemas.openxmlformats.org/markup-compatibility/2006" + mc_ignorable = "x" + + attributes = [ + ("xmlns", xmlns), + ("xmlns:mc", xmlns_mc), + ("mc:Ignorable", mc_ignorable), + ("xmlns:x", xmlns_x), + ] + + self._xml_start_tag("rvTypesInfo", attributes) + + def _write_global(self): + # Write the <global> element. + key_flags = [ + ["_Self", ["ExcludeFromFile", "ExcludeFromCalcComparison"]], + ["_DisplayString", ["ExcludeFromCalcComparison"]], + ["_Flags", ["ExcludeFromCalcComparison"]], + ["_Format", ["ExcludeFromCalcComparison"]], + ["_SubLabel", ["ExcludeFromCalcComparison"]], + ["_Attribution", ["ExcludeFromCalcComparison"]], + ["_Icon", ["ExcludeFromCalcComparison"]], + ["_Display", ["ExcludeFromCalcComparison"]], + ["_CanonicalPropertyNames", ["ExcludeFromCalcComparison"]], + ["_ClassificationId", ["ExcludeFromCalcComparison"]], + ] + + self._xml_start_tag("global") + self._xml_start_tag("keyFlags") + + for key_flag in key_flags: + # Write the key element. + self._write_key(key_flag) + + self._xml_end_tag("keyFlags") + self._xml_end_tag("global") + + def _write_key(self, key_flag): + # Write the <key> element. + name = key_flag[0] + attributes = [("name", name)] + + self._xml_start_tag("key", attributes) + + # Write the flag element. + for name in key_flag[1]: + self._write_flag(name) + + self._xml_end_tag("key") + + def _write_flag(self, name): + # Write the <flag> element. + attributes = [ + ("name", name), + ("value", "1"), + ] + + self._xml_empty_tag("flag", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/shape.py b/.venv/lib/python3.12/site-packages/xlsxwriter/shape.py new file mode 100644 index 00000000..8ad3676f --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/shape.py @@ -0,0 +1,416 @@ +############################################################################### +# +# Shape - A class for to represent Excel XLSX shape objects. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# +import copy +from warnings import warn + + +class Shape: + """ + A class for to represent Excel XLSX shape objects. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self, shape_type, name, options): + """ + Constructor. + + """ + super().__init__() + self.name = name + self.shape_type = shape_type + self.connect = 0 + self.drawing = 0 + self.edit_as = "" + self.id = 0 + self.text = "" + self.textlink = "" + self.stencil = 1 + self.element = -1 + self.start = None + self.start_index = None + self.end = None + self.end_index = None + self.adjustments = [] + self.start_side = "" + self.end_side = "" + self.flip_h = 0 + self.flip_v = 0 + self.rotation = 0 + self.text_rotation = 0 + self.textbox = False + + self.align = None + self.fill = None + self.font = None + self.format = None + self.line = None + self.url_rel_index = None + self.tip = None + + self._set_options(options) + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _set_options(self, options): + self.align = self._get_align_properties(options.get("align")) + self.fill = self._get_fill_properties(options.get("fill")) + self.font = self._get_font_properties(options.get("font")) + self.gradient = self._get_gradient_properties(options.get("gradient")) + self.line = self._get_line_properties(options.get("line")) + + self.text_rotation = options.get("text_rotation", 0) + + self.textlink = options.get("textlink", "") + if self.textlink.startswith("="): + self.textlink = self.textlink.lstrip("=") + + if options.get("border"): + self.line = self._get_line_properties(options["border"]) + + # Gradient fill overrides solid fill. + if self.gradient: + self.fill = None + + ########################################################################### + # + # Static methods for processing chart/shape style properties. + # + ########################################################################### + + @staticmethod + def _get_line_properties(line): + # Convert user line properties to the structure required internally. + + if not line: + return {"defined": False} + + # Copy the user defined properties since they will be modified. + line = copy.deepcopy(line) + + dash_types = { + "solid": "solid", + "round_dot": "sysDot", + "square_dot": "sysDash", + "dash": "dash", + "dash_dot": "dashDot", + "long_dash": "lgDash", + "long_dash_dot": "lgDashDot", + "long_dash_dot_dot": "lgDashDotDot", + "dot": "dot", + "system_dash_dot": "sysDashDot", + "system_dash_dot_dot": "sysDashDotDot", + } + + # Check the dash type. + dash_type = line.get("dash_type") + + if dash_type is not None: + if dash_type in dash_types: + line["dash_type"] = dash_types[dash_type] + else: + warn(f"Unknown dash type '{dash_type}'") + return {} + + line["defined"] = True + + return line + + @staticmethod + def _get_fill_properties(fill): + # Convert user fill properties to the structure required internally. + + if not fill: + return {"defined": False} + + # Copy the user defined properties since they will be modified. + fill = copy.deepcopy(fill) + + fill["defined"] = True + + return fill + + @staticmethod + def _get_pattern_properties(pattern): + # Convert user defined pattern to the structure required internally. + + if not pattern: + return {} + + # Copy the user defined properties since they will be modified. + pattern = copy.deepcopy(pattern) + + if not pattern.get("pattern"): + warn("Pattern must include 'pattern'") + return {} + + if not pattern.get("fg_color"): + warn("Pattern must include 'fg_color'") + return {} + + types = { + "percent_5": "pct5", + "percent_10": "pct10", + "percent_20": "pct20", + "percent_25": "pct25", + "percent_30": "pct30", + "percent_40": "pct40", + "percent_50": "pct50", + "percent_60": "pct60", + "percent_70": "pct70", + "percent_75": "pct75", + "percent_80": "pct80", + "percent_90": "pct90", + "light_downward_diagonal": "ltDnDiag", + "light_upward_diagonal": "ltUpDiag", + "dark_downward_diagonal": "dkDnDiag", + "dark_upward_diagonal": "dkUpDiag", + "wide_downward_diagonal": "wdDnDiag", + "wide_upward_diagonal": "wdUpDiag", + "light_vertical": "ltVert", + "light_horizontal": "ltHorz", + "narrow_vertical": "narVert", + "narrow_horizontal": "narHorz", + "dark_vertical": "dkVert", + "dark_horizontal": "dkHorz", + "dashed_downward_diagonal": "dashDnDiag", + "dashed_upward_diagonal": "dashUpDiag", + "dashed_horizontal": "dashHorz", + "dashed_vertical": "dashVert", + "small_confetti": "smConfetti", + "large_confetti": "lgConfetti", + "zigzag": "zigZag", + "wave": "wave", + "diagonal_brick": "diagBrick", + "horizontal_brick": "horzBrick", + "weave": "weave", + "plaid": "plaid", + "divot": "divot", + "dotted_grid": "dotGrid", + "dotted_diamond": "dotDmnd", + "shingle": "shingle", + "trellis": "trellis", + "sphere": "sphere", + "small_grid": "smGrid", + "large_grid": "lgGrid", + "small_check": "smCheck", + "large_check": "lgCheck", + "outlined_diamond": "openDmnd", + "solid_diamond": "solidDmnd", + } + + # Check for valid types. + if pattern["pattern"] not in types: + warn(f"unknown pattern type '{pattern['pattern']}'") + return {} + + pattern["pattern"] = types[pattern["pattern"]] + + # Specify a default background color. + pattern["bg_color"] = pattern.get("bg_color", "#FFFFFF") + + return pattern + + @staticmethod + def _get_gradient_properties(gradient): + # pylint: disable=too-many-return-statements + # Convert user defined gradient to the structure required internally. + + if not gradient: + return {} + + # Copy the user defined properties since they will be modified. + gradient = copy.deepcopy(gradient) + + types = { + "linear": "linear", + "radial": "circle", + "rectangular": "rect", + "path": "shape", + } + + # Check the colors array exists and is valid. + if "colors" not in gradient or not isinstance(gradient["colors"], list): + warn("Gradient must include colors list") + return {} + + # Check the colors array has the required number of entries. + if not 2 <= len(gradient["colors"]) <= 10: + warn("Gradient colors list must at least 2 values and not more than 10") + return {} + + if "positions" in gradient: + # Check the positions array has the right number of entries. + if len(gradient["positions"]) != len(gradient["colors"]): + warn("Gradient positions not equal to number of colors") + return {} + + # Check the positions are in the correct range. + for pos in gradient["positions"]: + if not 0 <= pos <= 100: + warn("Gradient position must be in the range 0 <= position <= 100") + return {} + else: + # Use the default gradient positions. + if len(gradient["colors"]) == 2: + gradient["positions"] = [0, 100] + + elif len(gradient["colors"]) == 3: + gradient["positions"] = [0, 50, 100] + + elif len(gradient["colors"]) == 4: + gradient["positions"] = [0, 33, 66, 100] + + else: + warn("Must specify gradient positions") + return {} + + angle = gradient.get("angle") + if angle: + if not 0 <= angle < 360: + warn("Gradient angle must be in the range 0 <= angle < 360") + return {} + else: + gradient["angle"] = 90 + + # Check for valid types. + gradient_type = gradient.get("type") + + if gradient_type is not None: + if gradient_type in types: + gradient["type"] = types[gradient_type] + else: + warn(f"Unknown gradient type '{gradient_type}") + return {} + else: + gradient["type"] = "linear" + + return gradient + + @staticmethod + def _get_font_properties(options): + # Convert user defined font values into private dict values. + if options is None: + options = {} + + font = { + "name": options.get("name"), + "color": options.get("color"), + "size": options.get("size", 11), + "bold": options.get("bold"), + "italic": options.get("italic"), + "underline": options.get("underline"), + "pitch_family": options.get("pitch_family"), + "charset": options.get("charset"), + "baseline": options.get("baseline", -1), + "lang": options.get("lang", "en-US"), + } + + # Convert font size units. + if font["size"]: + font["size"] = int(font["size"] * 100) + + return font + + @staticmethod + def _get_font_style_attributes(font): + # _get_font_style_attributes. + attributes = [] + + if not font: + return attributes + + if font.get("size"): + attributes.append(("sz", font["size"])) + + if font.get("bold") is not None: + attributes.append(("b", 0 + font["bold"])) + + if font.get("italic") is not None: + attributes.append(("i", 0 + font["italic"])) + + if font.get("underline") is not None: + attributes.append(("u", "sng")) + + if font.get("baseline") != -1: + attributes.append(("baseline", font["baseline"])) + + return attributes + + @staticmethod + def _get_font_latin_attributes(font): + # _get_font_latin_attributes. + attributes = [] + + if not font: + return attributes + + if font["name"] is not None: + attributes.append(("typeface", font["name"])) + + if font["pitch_family"] is not None: + attributes.append(("pitchFamily", font["pitch_family"])) + + if font["charset"] is not None: + attributes.append(("charset", font["charset"])) + + return attributes + + @staticmethod + def _get_align_properties(align): + # Convert user defined align to the structure required internally. + if not align: + return {"defined": False} + + # Copy the user defined properties since they will be modified. + align = copy.deepcopy(align) + + if "vertical" in align: + align_type = align["vertical"] + + align_types = { + "top": "top", + "middle": "middle", + "bottom": "bottom", + } + + if align_type in align_types: + align["vertical"] = align_types[align_type] + else: + warn(f"Unknown alignment type '{align_type}'") + return {"defined": False} + + if "horizontal" in align: + align_type = align["horizontal"] + + align_types = { + "left": "left", + "center": "center", + "right": "right", + } + + if align_type in align_types: + align["horizontal"] = align_types[align_type] + else: + warn(f"Unknown alignment type '{align_type}'") + return {"defined": False} + + align["defined"] = True + + return align diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/sharedstrings.py b/.venv/lib/python3.12/site-packages/xlsxwriter/sharedstrings.py new file mode 100644 index 00000000..df65c2ed --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/sharedstrings.py @@ -0,0 +1,138 @@ +############################################################################### +# +# SharedStrings - A class for writing the Excel XLSX sharedStrings file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Package imports. +from . import xmlwriter +from .utility import _preserve_whitespace + + +class SharedStrings(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX sharedStrings file. + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + self.string_table = None + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + # Write the sst element. + self._write_sst() + + # Write the sst strings. + self._write_sst_strings() + + # Close the sst tag. + self._xml_end_tag("sst") + + # Close the file. + self._xml_close() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_sst(self): + # Write the <sst> element. + xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" + + attributes = [ + ("xmlns", xmlns), + ("count", self.string_table.count), + ("uniqueCount", self.string_table.unique_count), + ] + + self._xml_start_tag("sst", attributes) + + def _write_sst_strings(self): + # Write the sst string elements. + + for string in self.string_table.string_array: + self._write_si(string) + + def _write_si(self, string): + # Write the <si> element. + attributes = [] + + # Convert control character to a _xHHHH_ escape. + string = self._escape_control_characters(string) + + # Add attribute to preserve leading or trailing whitespace. + if _preserve_whitespace(string): + attributes.append(("xml:space", "preserve")) + + # Write any rich strings without further tags. + if string.startswith("<r>") and string.endswith("</r>"): + self._xml_rich_si_element(string) + else: + self._xml_si_element(string, attributes) + + +# A metadata class to store Excel strings between worksheets. +class SharedStringTable: + """ + A class to track Excel shared strings between worksheets. + + """ + + def __init__(self): + self.count = 0 + self.unique_count = 0 + self.string_table = {} + self.string_array = [] + + def _get_shared_string_index(self, string): + """ " Get the index of the string in the Shared String table.""" + if string not in self.string_table: + # String isn't already stored in the table so add it. + index = self.unique_count + self.string_table[string] = index + self.count += 1 + self.unique_count += 1 + return index + + # String exists in the table. + index = self.string_table[string] + self.count += 1 + return index + + def _get_shared_string(self, index): + """ " Get a shared string from the index.""" + return self.string_array[index] + + def _sort_string_data(self): + """ " Sort the shared string data and convert from dict to list.""" + self.string_array = sorted(self.string_table, key=self.string_table.__getitem__) + self.string_table = {} diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/styles.py b/.venv/lib/python3.12/site-packages/xlsxwriter/styles.py new file mode 100644 index 00000000..a14035a9 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/styles.py @@ -0,0 +1,803 @@ +############################################################################### +# +# Styles - A class for writing the Excel XLSX Worksheet file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Package imports. +from . import xmlwriter + + +class Styles(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX Styles file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + self.xf_formats = [] + self.palette = [] + self.font_count = 0 + self.num_formats = [] + self.border_count = 0 + self.fill_count = 0 + self.custom_colors = [] + self.dxf_formats = [] + self.has_hyperlink = False + self.hyperlink_font_id = 0 + self.has_comments = False + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + # Add the style sheet. + self._write_style_sheet() + + # Write the number formats. + self._write_num_fmts() + + # Write the fonts. + self._write_fonts() + + # Write the fills. + self._write_fills() + + # Write the borders element. + self._write_borders() + + # Write the cellStyleXfs element. + self._write_cell_style_xfs() + + # Write the cellXfs element. + self._write_cell_xfs() + + # Write the cellStyles element. + self._write_cell_styles() + + # Write the dxfs element. + self._write_dxfs() + + # Write the tableStyles element. + self._write_table_styles() + + # Write the colors element. + self._write_colors() + + # Close the style sheet tag. + self._xml_end_tag("styleSheet") + + # Close the file. + self._xml_close() + + def _set_style_properties(self, properties): + # Pass in the Format objects and other properties used in the styles. + + self.xf_formats = properties[0] + self.palette = properties[1] + self.font_count = properties[2] + self.num_formats = properties[3] + self.border_count = properties[4] + self.fill_count = properties[5] + self.custom_colors = properties[6] + self.dxf_formats = properties[7] + self.has_comments = properties[8] + + def _get_palette_color(self, color): + # Special handling for automatic color. + if color == "Automatic": + return color + + # Convert the RGB color. + if color[0] == "#": + color = color[1:] + + return "FF" + color.upper() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_style_sheet(self): + # Write the <styleSheet> element. + xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" + + attributes = [("xmlns", xmlns)] + self._xml_start_tag("styleSheet", attributes) + + def _write_num_fmts(self): + # Write the <numFmts> element. + if not self.num_formats: + return + + attributes = [("count", len(self.num_formats))] + self._xml_start_tag("numFmts", attributes) + + # Write the numFmts elements. + for index, num_format in enumerate(self.num_formats, 164): + self._write_num_fmt(index, num_format) + + self._xml_end_tag("numFmts") + + def _write_num_fmt(self, num_fmt_id, format_code): + # Write the <numFmt> element. + format_codes = { + 0: "General", + 1: "0", + 2: "0.00", + 3: "#,##0", + 4: "#,##0.00", + 5: "($#,##0_);($#,##0)", + 6: "($#,##0_);[Red]($#,##0)", + 7: "($#,##0.00_);($#,##0.00)", + 8: "($#,##0.00_);[Red]($#,##0.00)", + 9: "0%", + 10: "0.00%", + 11: "0.00E+00", + 12: "# ?/?", + 13: "# ??/??", + 14: "m/d/yy", + 15: "d-mmm-yy", + 16: "d-mmm", + 17: "mmm-yy", + 18: "h:mm AM/PM", + 19: "h:mm:ss AM/PM", + 20: "h:mm", + 21: "h:mm:ss", + 22: "m/d/yy h:mm", + 37: "(#,##0_);(#,##0)", + 38: "(#,##0_);[Red](#,##0)", + 39: "(#,##0.00_);(#,##0.00)", + 40: "(#,##0.00_);[Red](#,##0.00)", + 41: '_(* #,##0_);_(* (#,##0);_(* "-"_);_(_)', + 42: '_($* #,##0_);_($* (#,##0);_($* "-"_);_(_)', + 43: '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(_)', + 44: '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(_)', + 45: "mm:ss", + 46: "[h]:mm:ss", + 47: "mm:ss.0", + 48: "##0.0E+0", + 49: "@", + } + + # Set the format code for built-in number formats. + if num_fmt_id < 164: + format_code = format_codes.get(num_fmt_id, "General") + + attributes = [ + ("numFmtId", num_fmt_id), + ("formatCode", format_code), + ] + + self._xml_empty_tag("numFmt", attributes) + + def _write_fonts(self): + # Write the <fonts> element. + if self.has_comments: + # Add extra font for comments. + attributes = [("count", self.font_count + 1)] + else: + attributes = [("count", self.font_count)] + + self._xml_start_tag("fonts", attributes) + + # Write the font elements for xf_format objects that have them. + for xf_format in self.xf_formats: + if xf_format.has_font: + self._write_font(xf_format) + + if self.has_comments: + self._write_comment_font() + + self._xml_end_tag("fonts") + + def _write_font(self, xf_format, is_dxf_format=False): + # Write the <font> element. + self._xml_start_tag("font") + + # The condense and extend elements are mainly used in dxf formats. + if xf_format.font_condense: + self._write_condense() + + if xf_format.font_extend: + self._write_extend() + + if xf_format.bold: + self._xml_empty_tag("b") + + if xf_format.italic: + self._xml_empty_tag("i") + + if xf_format.font_strikeout: + self._xml_empty_tag("strike") + + if xf_format.font_outline: + self._xml_empty_tag("outline") + + if xf_format.font_shadow: + self._xml_empty_tag("shadow") + + # Handle the underline variants. + if xf_format.underline: + self._write_underline(xf_format.underline) + + if xf_format.font_script == 1: + self._write_vert_align("superscript") + + if xf_format.font_script == 2: + self._write_vert_align("subscript") + + if not is_dxf_format: + self._xml_empty_tag("sz", [("val", xf_format.font_size)]) + + if xf_format.theme == -1: + # Ignore for excel2003_style. + pass + elif xf_format.theme: + self._write_color("theme", xf_format.theme) + elif xf_format.color_indexed: + self._write_color("indexed", xf_format.color_indexed) + elif xf_format.font_color: + color = self._get_palette_color(xf_format.font_color) + if color != "Automatic": + self._write_color("rgb", color) + elif not is_dxf_format: + self._write_color("theme", 1) + + if not is_dxf_format: + self._xml_empty_tag("name", [("val", xf_format.font_name)]) + + if xf_format.font_family: + self._xml_empty_tag("family", [("val", xf_format.font_family)]) + + if xf_format.font_charset: + self._xml_empty_tag("charset", [("val", xf_format.font_charset)]) + + if xf_format.font_name == "Calibri" and not xf_format.hyperlink: + self._xml_empty_tag("scheme", [("val", xf_format.font_scheme)]) + + if xf_format.hyperlink: + self.has_hyperlink = True + if self.hyperlink_font_id == 0: + self.hyperlink_font_id = xf_format.font_index + + self._xml_end_tag("font") + + def _write_comment_font(self): + # Write the <font> element for comments. + self._xml_start_tag("font") + + self._xml_empty_tag("sz", [("val", 8)]) + self._write_color("indexed", 81) + self._xml_empty_tag("name", [("val", "Tahoma")]) + self._xml_empty_tag("family", [("val", 2)]) + + self._xml_end_tag("font") + + def _write_underline(self, underline): + # Write the underline font element. + + if underline == 2: + attributes = [("val", "double")] + elif underline == 33: + attributes = [("val", "singleAccounting")] + elif underline == 34: + attributes = [("val", "doubleAccounting")] + else: + # Default to single underline. + attributes = [] + + self._xml_empty_tag("u", attributes) + + def _write_vert_align(self, val): + # Write the <vertAlign> font sub-element. + attributes = [("val", val)] + + self._xml_empty_tag("vertAlign", attributes) + + def _write_color(self, name, value): + # Write the <color> element. + attributes = [(name, value)] + + self._xml_empty_tag("color", attributes) + + def _write_fills(self): + # Write the <fills> element. + attributes = [("count", self.fill_count)] + + self._xml_start_tag("fills", attributes) + + # Write the default fill element. + self._write_default_fill("none") + self._write_default_fill("gray125") + + # Write the fill elements for xf_format objects that have them. + for xf_format in self.xf_formats: + if xf_format.has_fill: + self._write_fill(xf_format) + + self._xml_end_tag("fills") + + def _write_default_fill(self, pattern_type): + # Write the <fill> element for the default fills. + self._xml_start_tag("fill") + self._xml_empty_tag("patternFill", [("patternType", pattern_type)]) + self._xml_end_tag("fill") + + def _write_fill(self, xf_format, is_dxf_format=False): + # Write the <fill> element. + pattern = xf_format.pattern + bg_color = xf_format.bg_color + fg_color = xf_format.fg_color + + # Colors for dxf formats are handled differently from normal formats + # since the normal xf_format reverses the meaning of BG and FG for + # solid fills. + if is_dxf_format: + bg_color = xf_format.dxf_bg_color + fg_color = xf_format.dxf_fg_color + + patterns = ( + "none", + "solid", + "mediumGray", + "darkGray", + "lightGray", + "darkHorizontal", + "darkVertical", + "darkDown", + "darkUp", + "darkGrid", + "darkTrellis", + "lightHorizontal", + "lightVertical", + "lightDown", + "lightUp", + "lightGrid", + "lightTrellis", + "gray125", + "gray0625", + ) + + # Special handling for pattern only case. + if not fg_color and not bg_color and patterns[pattern]: + self._write_default_fill(patterns[pattern]) + return + + self._xml_start_tag("fill") + + # The "none" pattern is handled differently for dxf formats. + if is_dxf_format and pattern <= 1: + self._xml_start_tag("patternFill") + else: + self._xml_start_tag("patternFill", [("patternType", patterns[pattern])]) + + if fg_color: + fg_color = self._get_palette_color(fg_color) + if fg_color != "Automatic": + self._xml_empty_tag("fgColor", [("rgb", fg_color)]) + + if bg_color: + bg_color = self._get_palette_color(bg_color) + if bg_color != "Automatic": + self._xml_empty_tag("bgColor", [("rgb", bg_color)]) + else: + if not is_dxf_format and pattern <= 1: + self._xml_empty_tag("bgColor", [("indexed", 64)]) + + self._xml_end_tag("patternFill") + self._xml_end_tag("fill") + + def _write_borders(self): + # Write the <borders> element. + attributes = [("count", self.border_count)] + + self._xml_start_tag("borders", attributes) + + # Write the border elements for xf_format objects that have them. + for xf_format in self.xf_formats: + if xf_format.has_border: + self._write_border(xf_format) + + self._xml_end_tag("borders") + + def _write_border(self, xf_format, is_dxf_format=False): + # Write the <border> element. + attributes = [] + + # Diagonal borders add attributes to the <border> element. + if xf_format.diag_type == 1: + attributes.append(("diagonalUp", 1)) + elif xf_format.diag_type == 2: + attributes.append(("diagonalDown", 1)) + elif xf_format.diag_type == 3: + attributes.append(("diagonalUp", 1)) + attributes.append(("diagonalDown", 1)) + + # Ensure that a default diag border is set if the diag type is set. + if xf_format.diag_type and not xf_format.diag_border: + xf_format.diag_border = 1 + + # Write the start border tag. + self._xml_start_tag("border", attributes) + + # Write the <border> sub elements. + self._write_sub_border("left", xf_format.left, xf_format.left_color) + + self._write_sub_border("right", xf_format.right, xf_format.right_color) + + self._write_sub_border("top", xf_format.top, xf_format.top_color) + + self._write_sub_border("bottom", xf_format.bottom, xf_format.bottom_color) + + # Condition DXF formats don't allow diagonal borders. + if not is_dxf_format: + self._write_sub_border( + "diagonal", xf_format.diag_border, xf_format.diag_color + ) + + if is_dxf_format: + self._write_sub_border("vertical", None, None) + self._write_sub_border("horizontal", None, None) + + self._xml_end_tag("border") + + def _write_sub_border(self, border_type, style, color): + # Write the <border> sub elements such as <right>, <top>, etc. + attributes = [] + + if not style: + self._xml_empty_tag(border_type) + return + + border_styles = ( + "none", + "thin", + "medium", + "dashed", + "dotted", + "thick", + "double", + "hair", + "mediumDashed", + "dashDot", + "mediumDashDot", + "dashDotDot", + "mediumDashDotDot", + "slantDashDot", + ) + + attributes.append(("style", border_styles[style])) + + self._xml_start_tag(border_type, attributes) + + if color and color != "Automatic": + color = self._get_palette_color(color) + self._xml_empty_tag("color", [("rgb", color)]) + else: + self._xml_empty_tag("color", [("auto", 1)]) + + self._xml_end_tag(border_type) + + def _write_cell_style_xfs(self): + # Write the <cellStyleXfs> element. + count = 1 + + if self.has_hyperlink: + count = 2 + + attributes = [("count", count)] + + self._xml_start_tag("cellStyleXfs", attributes) + self._write_style_xf() + + if self.has_hyperlink: + self._write_style_xf(True, self.hyperlink_font_id) + + self._xml_end_tag("cellStyleXfs") + + def _write_cell_xfs(self): + # Write the <cellXfs> element. + formats = self.xf_formats + + # Workaround for when the last xf_format is used for the comment font + # and shouldn't be used for cellXfs. + last_format = formats[-1] + if last_format.font_only: + formats.pop() + + attributes = [("count", len(formats))] + self._xml_start_tag("cellXfs", attributes) + + # Write the xf elements. + for xf_format in formats: + self._write_xf(xf_format) + + self._xml_end_tag("cellXfs") + + def _write_style_xf(self, has_hyperlink=False, font_id=0): + # Write the style <xf> element. + num_fmt_id = 0 + fill_id = 0 + border_id = 0 + + attributes = [ + ("numFmtId", num_fmt_id), + ("fontId", font_id), + ("fillId", fill_id), + ("borderId", border_id), + ] + + if has_hyperlink: + attributes.append(("applyNumberFormat", 0)) + attributes.append(("applyFill", 0)) + attributes.append(("applyBorder", 0)) + attributes.append(("applyAlignment", 0)) + attributes.append(("applyProtection", 0)) + + self._xml_start_tag("xf", attributes) + self._xml_empty_tag("alignment", [("vertical", "top")]) + self._xml_empty_tag("protection", [("locked", 0)]) + self._xml_end_tag("xf") + + else: + self._xml_empty_tag("xf", attributes) + + def _write_xf(self, xf_format): + # Write the <xf> element. + xf_id = xf_format.xf_id + font_id = xf_format.font_index + fill_id = xf_format.fill_index + border_id = xf_format.border_index + num_fmt_id = xf_format.num_format_index + + has_checkbox = xf_format.checkbox + has_alignment = False + has_protection = False + + attributes = [ + ("numFmtId", num_fmt_id), + ("fontId", font_id), + ("fillId", fill_id), + ("borderId", border_id), + ("xfId", xf_id), + ] + + if xf_format.quote_prefix: + attributes.append(("quotePrefix", 1)) + + if xf_format.num_format_index > 0: + attributes.append(("applyNumberFormat", 1)) + + # Add applyFont attribute if XF format uses a font element. + if xf_format.font_index > 0 and not xf_format.hyperlink: + attributes.append(("applyFont", 1)) + + # Add applyFill attribute if XF format uses a fill element. + if xf_format.fill_index > 0: + attributes.append(("applyFill", 1)) + + # Add applyBorder attribute if XF format uses a border element. + if xf_format.border_index > 0: + attributes.append(("applyBorder", 1)) + + # Check if XF format has alignment properties set. + (apply_align, align) = xf_format._get_align_properties() + + # Check if an alignment sub-element should be written. + if apply_align and align: + has_alignment = True + + # We can also have applyAlignment without a sub-element. + if apply_align or xf_format.hyperlink: + attributes.append(("applyAlignment", 1)) + + # Check for cell protection properties. + protection = xf_format._get_protection_properties() + + if protection or xf_format.hyperlink: + attributes.append(("applyProtection", 1)) + + if not xf_format.hyperlink: + has_protection = True + + # Write XF with sub-elements if required. + if has_alignment or has_protection or has_checkbox: + self._xml_start_tag("xf", attributes) + + if has_alignment: + self._xml_empty_tag("alignment", align) + + if has_protection: + self._xml_empty_tag("protection", protection) + + if has_checkbox: + self._write_xf_format_extensions() + + self._xml_end_tag("xf") + else: + self._xml_empty_tag("xf", attributes) + + def _write_cell_styles(self): + # Write the <cellStyles> element. + count = 1 + + if self.has_hyperlink: + count = 2 + + attributes = [("count", count)] + + self._xml_start_tag("cellStyles", attributes) + + if self.has_hyperlink: + self._write_cell_style("Hyperlink", 1, 8) + + self._write_cell_style() + + self._xml_end_tag("cellStyles") + + def _write_cell_style(self, name="Normal", xf_id=0, builtin_id=0): + # Write the <cellStyle> element. + attributes = [ + ("name", name), + ("xfId", xf_id), + ("builtinId", builtin_id), + ] + + self._xml_empty_tag("cellStyle", attributes) + + def _write_dxfs(self): + # Write the <dxfs> element. + formats = self.dxf_formats + count = len(formats) + + attributes = [("count", len(formats))] + + if count: + self._xml_start_tag("dxfs", attributes) + + # Write the font elements for xf_format objects that have them. + for dxf_format in self.dxf_formats: + self._xml_start_tag("dxf") + if dxf_format.has_dxf_font: + self._write_font(dxf_format, True) + + if dxf_format.num_format_index: + self._write_num_fmt( + dxf_format.num_format_index, dxf_format.num_format + ) + + if dxf_format.has_dxf_fill: + self._write_fill(dxf_format, True) + + if dxf_format.has_dxf_border: + self._write_border(dxf_format, True) + + if dxf_format.checkbox: + self._write_dxf_format_extensions() + + self._xml_end_tag("dxf") + + self._xml_end_tag("dxfs") + else: + self._xml_empty_tag("dxfs", attributes) + + def _write_table_styles(self): + # Write the <tableStyles> element. + count = 0 + default_table_style = "TableStyleMedium9" + default_pivot_style = "PivotStyleLight16" + + attributes = [ + ("count", count), + ("defaultTableStyle", default_table_style), + ("defaultPivotStyle", default_pivot_style), + ] + + self._xml_empty_tag("tableStyles", attributes) + + def _write_colors(self): + # Write the <colors> element. + custom_colors = self.custom_colors + + if not custom_colors: + return + + self._xml_start_tag("colors") + self._write_mru_colors(custom_colors) + self._xml_end_tag("colors") + + def _write_mru_colors(self, custom_colors): + # Write the <mruColors> element for the most recently used colors. + + # Write the custom custom_colors in reverse order. + custom_colors.reverse() + + # Limit the mruColors to the last 10. + if len(custom_colors) > 10: + custom_colors = custom_colors[0:10] + + self._xml_start_tag("mruColors") + + # Write the custom custom_colors in reverse order. + for color in custom_colors: + self._write_color("rgb", color) + + self._xml_end_tag("mruColors") + + def _write_condense(self): + # Write the <condense> element. + attributes = [("val", 0)] + + self._xml_empty_tag("condense", attributes) + + def _write_extend(self): + # Write the <extend> element. + attributes = [("val", 0)] + + self._xml_empty_tag("extend", attributes) + + def _write_xf_format_extensions(self): + # Write the xfComplement <extLst> elements. + schema = "http://schemas.microsoft.com/office/spreadsheetml" + attributes = [ + ("uri", "{C7286773-470A-42A8-94C5-96B5CB345126}"), + ( + "xmlns:xfpb", + schema + "/2022/featurepropertybag", + ), + ] + + self._xml_start_tag("extLst") + self._xml_start_tag("ext", attributes) + + self._xml_empty_tag("xfpb:xfComplement", [("i", "0")]) + + self._xml_end_tag("ext") + self._xml_end_tag("extLst") + + def _write_dxf_format_extensions(self): + # Write the DXFComplement <extLst> elements. + schema = "http://schemas.microsoft.com/office/spreadsheetml" + attributes = [ + ("uri", "{0417FA29-78FA-4A13-93AC-8FF0FAFDF519}"), + ( + "xmlns:xfpb", + schema + "/2022/featurepropertybag", + ), + ] + + self._xml_start_tag("extLst") + self._xml_start_tag("ext", attributes) + + self._xml_empty_tag("xfpb:DXFComplement", [("i", "0")]) + + self._xml_end_tag("ext") + self._xml_end_tag("extLst") diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/table.py b/.venv/lib/python3.12/site-packages/xlsxwriter/table.py new file mode 100644 index 00000000..f3e757a6 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/table.py @@ -0,0 +1,194 @@ +############################################################################### +# +# Table - A class for writing the Excel XLSX Worksheet file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from . import xmlwriter + + +class Table(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX Table file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + self.properties = {} + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + # Write the table element. + self._write_table() + + # Write the autoFilter element. + self._write_auto_filter() + + # Write the tableColumns element. + self._write_table_columns() + + # Write the tableStyleInfo element. + self._write_table_style_info() + + # Close the table tag. + self._xml_end_tag("table") + + # Close the file. + self._xml_close() + + def _set_properties(self, properties): + # Set the document properties. + self.properties = properties + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_table(self): + # Write the <table> element. + schema = "http://schemas.openxmlformats.org/" + xmlns = schema + "spreadsheetml/2006/main" + table_id = self.properties["id"] + name = self.properties["name"] + display_name = self.properties["name"] + ref = self.properties["range"] + totals_row_shown = self.properties["totals_row_shown"] + header_row_count = self.properties["header_row_count"] + + attributes = [ + ("xmlns", xmlns), + ("id", table_id), + ("name", name), + ("displayName", display_name), + ("ref", ref), + ] + + if not header_row_count: + attributes.append(("headerRowCount", 0)) + + if totals_row_shown: + attributes.append(("totalsRowCount", 1)) + else: + attributes.append(("totalsRowShown", 0)) + + self._xml_start_tag("table", attributes) + + def _write_auto_filter(self): + # Write the <autoFilter> element. + autofilter = self.properties.get("autofilter", 0) + + if not autofilter: + return + + attributes = [ + ( + "ref", + autofilter, + ) + ] + + self._xml_empty_tag("autoFilter", attributes) + + def _write_table_columns(self): + # Write the <tableColumns> element. + columns = self.properties["columns"] + + count = len(columns) + + attributes = [("count", count)] + + self._xml_start_tag("tableColumns", attributes) + + for col_data in columns: + # Write the tableColumn element. + self._write_table_column(col_data) + + self._xml_end_tag("tableColumns") + + def _write_table_column(self, col_data): + # Write the <tableColumn> element. + attributes = [ + ("id", col_data["id"]), + ("name", col_data["name"]), + ] + + if col_data.get("total_string"): + attributes.append(("totalsRowLabel", col_data["total_string"])) + elif col_data.get("total_function"): + attributes.append(("totalsRowFunction", col_data["total_function"])) + + if "format" in col_data and col_data["format"] is not None: + attributes.append(("dataDxfId", col_data["format"])) + + if col_data.get("formula") or col_data.get("custom_total"): + self._xml_start_tag("tableColumn", attributes) + + if col_data.get("formula"): + # Write the calculatedColumnFormula element. + self._write_calculated_column_formula(col_data["formula"]) + + if col_data.get("custom_total"): + # Write the totalsRowFormula element. + self._write_totals_row_formula(col_data.get("custom_total")) + + self._xml_end_tag("tableColumn") + else: + self._xml_empty_tag("tableColumn", attributes) + + def _write_table_style_info(self): + # Write the <tableStyleInfo> element. + props = self.properties + attributes = [] + + name = props["style"] + show_first_column = 0 + props["show_first_col"] + show_last_column = 0 + props["show_last_col"] + show_row_stripes = 0 + props["show_row_stripes"] + show_column_stripes = 0 + props["show_col_stripes"] + + if name is not None and name != "" and name != "None": + attributes.append(("name", name)) + + attributes.append(("showFirstColumn", show_first_column)) + attributes.append(("showLastColumn", show_last_column)) + attributes.append(("showRowStripes", show_row_stripes)) + attributes.append(("showColumnStripes", show_column_stripes)) + + self._xml_empty_tag("tableStyleInfo", attributes) + + def _write_calculated_column_formula(self, formula): + # Write the <calculatedColumnFormula> element. + self._xml_data_element("calculatedColumnFormula", formula) + + def _write_totals_row_formula(self, formula): + # Write the <totalsRowFormula> element. + self._xml_data_element("totalsRowFormula", formula) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/theme.py b/.venv/lib/python3.12/site-packages/xlsxwriter/theme.py new file mode 100644 index 00000000..71aec0dd --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/theme.py @@ -0,0 +1,69 @@ +############################################################################### +# +# Theme - A class for writing the Excel XLSX Worksheet file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +from io import StringIO + + +class Theme: + """ + A class for writing the Excel XLSX Theme file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + super().__init__() + self.fh = None + self.internal_fh = False + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + self._write_theme_file() + if self.internal_fh: + self.fh.close() + + def _set_xml_writer(self, filename): + # Set the XML writer filehandle for the object. + if isinstance(filename, StringIO): + self.internal_fh = False + self.fh = filename + else: + self.internal_fh = True + # pylint: disable=consider-using-with + self.fh = open(filename, mode="w", encoding="utf-8") + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_theme_file(self): + # Write a default theme.xml file. + + # pylint: disable=line-too-long + default_theme = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme"><a:themeElements><a:clrScheme name="Office"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="1F497D"/></a:dk2><a:lt2><a:srgbClr val="EEECE1"/></a:lt2><a:accent1><a:srgbClr val="4F81BD"/></a:accent1><a:accent2><a:srgbClr val="C0504D"/></a:accent2><a:accent3><a:srgbClr val="9BBB59"/></a:accent3><a:accent4><a:srgbClr val="8064A2"/></a:accent4><a:accent5><a:srgbClr val="4BACC6"/></a:accent5><a:accent6><a:srgbClr val="F79646"/></a:accent6><a:hlink><a:srgbClr val="0000FF"/></a:hlink><a:folHlink><a:srgbClr val="800080"/></a:folHlink></a:clrScheme><a:fontScheme name="Office"><a:majorFont><a:latin typeface="Cambria"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="\uff2d\uff33 \uff30\u30b4\u30b7\u30c3\u30af"/><a:font script="Hang" typeface="\ub9d1\uc740 \uace0\ub515"/><a:font script="Hans" typeface="\u5b8b\u4f53"/><a:font script="Hant" typeface="\u65b0\u7d30\u660e\u9ad4"/><a:font script="Arab" typeface="Times New Roman"/><a:font script="Hebr" typeface="Times New Roman"/><a:font script="Thai" typeface="Tahoma"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="MoolBoran"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Times New Roman"/><a:font script="Uigh" typeface="Microsoft Uighur"/></a:majorFont><a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="\uff2d\uff33 \uff30\u30b4\u30b7\u30c3\u30af"/><a:font script="Hang" typeface="\ub9d1\uc740 \uace0\ub515"/><a:font script="Hans" typeface="\u5b8b\u4f53"/><a:font script="Hant" typeface="\u65b0\u7d30\u660e\u9ad4"/><a:font script="Arab" typeface="Arial"/><a:font script="Hebr" typeface="Arial"/><a:font script="Thai" typeface="Tahoma"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="DaunPenh"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Arial"/><a:font script="Uigh" typeface="Microsoft Uighur"/></a:minorFont></a:fontScheme><a:fmtScheme name="Office"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="50000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="35000"><a:schemeClr val="phClr"><a:tint val="37000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="15000"/><a:satMod val="350000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="1"/></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:shade val="51000"/><a:satMod val="130000"/></a:schemeClr></a:gs><a:gs pos="80000"><a:schemeClr val="phClr"><a:shade val="93000"/><a:satMod val="130000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="94000"/><a:satMod val="135000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="0"/></a:gradFill></a:fillStyleLst><a:lnStyleLst><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"><a:shade val="95000"/><a:satMod val="105000"/></a:schemeClr></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="25400" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="38100" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="20000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="38000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst><a:scene3d><a:camera prst="orthographicFront"><a:rot lat="0" lon="0" rev="0"/></a:camera><a:lightRig rig="threePt" dir="t"><a:rot lat="0" lon="0" rev="1200000"/></a:lightRig></a:scene3d><a:sp3d><a:bevelT w="63500" h="25400"/></a:sp3d></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="40000"/><a:satMod val="350000"/></a:schemeClr></a:gs><a:gs pos="40000"><a:schemeClr val="phClr"><a:tint val="45000"/><a:shade val="99000"/><a:satMod val="350000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="20000"/><a:satMod val="255000"/></a:schemeClr></a:gs></a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="-80000" r="50000" b="180000"/></a:path></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="80000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="30000"/><a:satMod val="200000"/></a:schemeClr></a:gs></a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path></a:gradFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements><a:objectDefaults/><a:extraClrSchemeLst/></a:theme>""" # noqa + + self.fh.write(default_theme) 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 diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/vml.py b/.venv/lib/python3.12/site-packages/xlsxwriter/vml.py new file mode 100644 index 00000000..f97bc210 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/vml.py @@ -0,0 +1,707 @@ +############################################################################### +# +# Vml - A class for writing the Excel XLSX Vml file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Package imports. +from . import xmlwriter + + +class Vml(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX Vml file. + + + """ + + ########################################################################### + # + # Private API. + # + ########################################################################### + def _assemble_xml_file( + self, + data_id, + vml_shape_id, + comments_data=None, + buttons_data=None, + header_images_data=None, + ): + # Assemble and write the XML file. + z_index = 1 + + self._write_xml_namespace() + + # Write the o:shapelayout element. + self._write_shapelayout(data_id) + + if buttons_data: + # Write the v:shapetype element. + self._write_button_shapetype() + + for button in buttons_data: + # Write the v:shape element. + vml_shape_id += 1 + self._write_button_shape(vml_shape_id, z_index, button) + z_index += 1 + + if comments_data: + # Write the v:shapetype element. + self._write_comment_shapetype() + + for comment in comments_data: + # Write the v:shape element. + vml_shape_id += 1 + self._write_comment_shape(vml_shape_id, z_index, comment) + z_index += 1 + + if header_images_data: + # Write the v:shapetype element. + self._write_image_shapetype() + + index = 1 + for image in header_images_data: + # Write the v:shape element. + vml_shape_id += 1 + self._write_image_shape(vml_shape_id, index, image) + index += 1 + + self._xml_end_tag("xml") + + # Close the XML writer filehandle. + self._xml_close() + + def _pixels_to_points(self, vertices): + # Convert comment vertices from pixels to points. + + left, top, width, height = vertices[8:12] + + # Scale to pixels. + left *= 0.75 + top *= 0.75 + width *= 0.75 + height *= 0.75 + + return left, top, width, height + + ########################################################################### + # + # XML methods. + # + ########################################################################### + def _write_xml_namespace(self): + # Write the <xml> element. This is the root element of VML. + schema = "urn:schemas-microsoft-com:" + xmlns = schema + "vml" + xmlns_o = schema + "office:office" + xmlns_x = schema + "office:excel" + + attributes = [ + ("xmlns:v", xmlns), + ("xmlns:o", xmlns_o), + ("xmlns:x", xmlns_x), + ] + + self._xml_start_tag("xml", attributes) + + def _write_shapelayout(self, data_id): + # Write the <o:shapelayout> element. + attributes = [("v:ext", "edit")] + + self._xml_start_tag("o:shapelayout", attributes) + + # Write the o:idmap element. + self._write_idmap(data_id) + + self._xml_end_tag("o:shapelayout") + + def _write_idmap(self, data_id): + # Write the <o:idmap> element. + attributes = [ + ("v:ext", "edit"), + ("data", data_id), + ] + + self._xml_empty_tag("o:idmap", attributes) + + def _write_comment_shapetype(self): + # Write the <v:shapetype> element. + shape_id = "_x0000_t202" + coordsize = "21600,21600" + spt = 202 + path = "m,l,21600r21600,l21600,xe" + + attributes = [ + ("id", shape_id), + ("coordsize", coordsize), + ("o:spt", spt), + ("path", path), + ] + + self._xml_start_tag("v:shapetype", attributes) + + # Write the v:stroke element. + self._write_stroke() + + # Write the v:path element. + self._write_comment_path("t", "rect") + + self._xml_end_tag("v:shapetype") + + def _write_button_shapetype(self): + # Write the <v:shapetype> element. + shape_id = "_x0000_t201" + coordsize = "21600,21600" + spt = 201 + path = "m,l,21600r21600,l21600,xe" + + attributes = [ + ("id", shape_id), + ("coordsize", coordsize), + ("o:spt", spt), + ("path", path), + ] + + self._xml_start_tag("v:shapetype", attributes) + + # Write the v:stroke element. + self._write_stroke() + + # Write the v:path element. + self._write_button_path() + + # Write the o:lock element. + self._write_shapetype_lock() + + self._xml_end_tag("v:shapetype") + + def _write_image_shapetype(self): + # Write the <v:shapetype> element. + shape_id = "_x0000_t75" + coordsize = "21600,21600" + spt = 75 + o_preferrelative = "t" + path = "m@4@5l@4@11@9@11@9@5xe" + filled = "f" + stroked = "f" + + attributes = [ + ("id", shape_id), + ("coordsize", coordsize), + ("o:spt", spt), + ("o:preferrelative", o_preferrelative), + ("path", path), + ("filled", filled), + ("stroked", stroked), + ] + + self._xml_start_tag("v:shapetype", attributes) + + # Write the v:stroke element. + self._write_stroke() + + # Write the v:formulas element. + self._write_formulas() + + # Write the v:path element. + self._write_image_path() + + # Write the o:lock element. + self._write_aspect_ratio_lock() + + self._xml_end_tag("v:shapetype") + + def _write_stroke(self): + # Write the <v:stroke> element. + joinstyle = "miter" + + attributes = [("joinstyle", joinstyle)] + + self._xml_empty_tag("v:stroke", attributes) + + def _write_comment_path(self, gradientshapeok, connecttype): + # Write the <v:path> element. + attributes = [] + + if gradientshapeok: + attributes.append(("gradientshapeok", "t")) + + attributes.append(("o:connecttype", connecttype)) + + self._xml_empty_tag("v:path", attributes) + + def _write_button_path(self): + # Write the <v:path> element. + shadowok = "f" + extrusionok = "f" + strokeok = "f" + fillok = "f" + connecttype = "rect" + + attributes = [ + ("shadowok", shadowok), + ("o:extrusionok", extrusionok), + ("strokeok", strokeok), + ("fillok", fillok), + ("o:connecttype", connecttype), + ] + + self._xml_empty_tag("v:path", attributes) + + def _write_image_path(self): + # Write the <v:path> element. + extrusionok = "f" + gradientshapeok = "t" + connecttype = "rect" + + attributes = [ + ("o:extrusionok", extrusionok), + ("gradientshapeok", gradientshapeok), + ("o:connecttype", connecttype), + ] + + self._xml_empty_tag("v:path", attributes) + + def _write_shapetype_lock(self): + # Write the <o:lock> element. + ext = "edit" + shapetype = "t" + + attributes = [ + ("v:ext", ext), + ("shapetype", shapetype), + ] + + self._xml_empty_tag("o:lock", attributes) + + def _write_rotation_lock(self): + # Write the <o:lock> element. + ext = "edit" + rotation = "t" + + attributes = [ + ("v:ext", ext), + ("rotation", rotation), + ] + + self._xml_empty_tag("o:lock", attributes) + + def _write_aspect_ratio_lock(self): + # Write the <o:lock> element. + ext = "edit" + aspectratio = "t" + + attributes = [ + ("v:ext", ext), + ("aspectratio", aspectratio), + ] + + self._xml_empty_tag("o:lock", attributes) + + def _write_comment_shape(self, shape_id, z_index, comment): + # Write the <v:shape> element. + shape_type = "#_x0000_t202" + insetmode = "auto" + visibility = "hidden" + + # Set the shape index. + shape_id = "_x0000_s" + str(shape_id) + + # Get the comment parameters + row = comment[0] + col = comment[1] + visible = comment[4] + fillcolor = comment[5] + vertices = comment[9] + + (left, top, width, height) = self._pixels_to_points(vertices) + + # Set the visibility. + if visible: + visibility = "visible" + + style = ( + f"position:absolute;" + f"margin-left:{left:.15g}pt;" + f"margin-top:{top:.15g}pt;" + f"width:{width:.15g}pt;" + f"height:{height:.15g}pt;" + f"z-index:{z_index};" + f"visibility:{visibility}" + ) + + attributes = [ + ("id", shape_id), + ("type", shape_type), + ("style", style), + ("fillcolor", fillcolor), + ("o:insetmode", insetmode), + ] + + self._xml_start_tag("v:shape", attributes) + + # Write the v:fill element. + self._write_comment_fill() + + # Write the v:shadow element. + self._write_shadow() + + # Write the v:path element. + self._write_comment_path(None, "none") + + # Write the v:textbox element. + self._write_comment_textbox() + + # Write the x:ClientData element. + self._write_comment_client_data(row, col, visible, vertices) + + self._xml_end_tag("v:shape") + + def _write_button_shape(self, shape_id, z_index, button): + # Write the <v:shape> element. + shape_type = "#_x0000_t201" + + # Set the shape index. + shape_id = "_x0000_s" + str(shape_id) + + # Get the button parameters. + # row = button["_row"] + # col = button["_col"] + vertices = button["vertices"] + + (left, top, width, height) = self._pixels_to_points(vertices) + + style = ( + f"position:absolute;" + f"margin-left:{left:.15g}pt;" + f"margin-top:{top:.15g}pt;" + f"width:{width:.15g}pt;" + f"height:{height:.15g}pt;" + f"z-index:{z_index};" + f"mso-wrap-style:tight" + ) + + attributes = [ + ("id", shape_id), + ("type", shape_type), + ] + + if button.get("description"): + attributes.append(("alt", button["description"])) + + attributes.append(("style", style)) + attributes.append(("o:button", "t")) + attributes.append(("fillcolor", "buttonFace [67]")) + attributes.append(("strokecolor", "windowText [64]")) + attributes.append(("o:insetmode", "auto")) + + self._xml_start_tag("v:shape", attributes) + + # Write the v:fill element. + self._write_button_fill() + + # Write the o:lock element. + self._write_rotation_lock() + + # Write the v:textbox element. + self._write_button_textbox(button["font"]) + + # Write the x:ClientData element. + self._write_button_client_data(button) + + self._xml_end_tag("v:shape") + + def _write_image_shape(self, shape_id, z_index, image_data): + # Write the <v:shape> element. + shape_type = "#_x0000_t75" + + # Set the shape index. + shape_id = "_x0000_s" + str(shape_id) + + # Get the image parameters + width = image_data[0] + height = image_data[1] + name = image_data[2] + position = image_data[3] + x_dpi = image_data[4] + y_dpi = image_data[5] + ref_id = image_data[6] + + # Scale the height/width by the resolution, relative to 72dpi. + width = width * 72.0 / x_dpi + height = height * 72.0 / y_dpi + + # Excel uses a rounding based around 72 and 96 dpi. + width = 72.0 / 96 * int(width * 96.0 / 72 + 0.25) + height = 72.0 / 96 * int(height * 96.0 / 72 + 0.25) + + style = ( + f"position:absolute;" + f"margin-left:0;" + f"margin-top:0;" + f"width:{width:.15g}pt;" + f"height:{height:.15g}pt;" + f"z-index:{z_index}" + ) + + attributes = [ + ("id", position), + ("o:spid", shape_id), + ("type", shape_type), + ("style", style), + ] + + self._xml_start_tag("v:shape", attributes) + + # Write the v:imagedata element. + self._write_imagedata(ref_id, name) + + # Write the o:lock element. + self._write_rotation_lock() + + self._xml_end_tag("v:shape") + + def _write_comment_fill(self): + # Write the <v:fill> element. + color_2 = "#ffffe1" + + attributes = [("color2", color_2)] + + self._xml_empty_tag("v:fill", attributes) + + def _write_button_fill(self): + # Write the <v:fill> element. + color_2 = "buttonFace [67]" + detectmouseclick = "t" + + attributes = [ + ("color2", color_2), + ("o:detectmouseclick", detectmouseclick), + ] + + self._xml_empty_tag("v:fill", attributes) + + def _write_shadow(self): + # Write the <v:shadow> element. + on = "t" + color = "black" + obscured = "t" + + attributes = [ + ("on", on), + ("color", color), + ("obscured", obscured), + ] + + self._xml_empty_tag("v:shadow", attributes) + + def _write_comment_textbox(self): + # Write the <v:textbox> element. + style = "mso-direction-alt:auto" + + attributes = [("style", style)] + + self._xml_start_tag("v:textbox", attributes) + + # Write the div element. + self._write_div("left") + + self._xml_end_tag("v:textbox") + + def _write_button_textbox(self, font): + # Write the <v:textbox> element. + style = "mso-direction-alt:auto" + + attributes = [("style", style), ("o:singleclick", "f")] + + self._xml_start_tag("v:textbox", attributes) + + # Write the div element. + self._write_div("center", font) + + self._xml_end_tag("v:textbox") + + def _write_div(self, align, font=None): + # Write the <div> element. + + style = "text-align:" + align + + attributes = [("style", style)] + + self._xml_start_tag("div", attributes) + + if font: + # Write the font element. + self._write_font(font) + + self._xml_end_tag("div") + + def _write_font(self, font): + # Write the <font> element. + caption = font["caption"] + face = "Calibri" + size = 220 + color = "#000000" + + attributes = [ + ("face", face), + ("size", size), + ("color", color), + ] + + self._xml_data_element("font", caption, attributes) + + def _write_comment_client_data(self, row, col, visible, vertices): + # Write the <x:ClientData> element. + object_type = "Note" + + attributes = [("ObjectType", object_type)] + + self._xml_start_tag("x:ClientData", attributes) + + # Write the x:MoveWithCells element. + self._write_move_with_cells() + + # Write the x:SizeWithCells element. + self._write_size_with_cells() + + # Write the x:Anchor element. + self._write_anchor(vertices) + + # Write the x:AutoFill element. + self._write_auto_fill() + + # Write the x:Row element. + self._write_row(row) + + # Write the x:Column element. + self._write_column(col) + + # Write the x:Visible element. + if visible: + self._write_visible() + + self._xml_end_tag("x:ClientData") + + def _write_button_client_data(self, button): + # Write the <x:ClientData> element. + macro = button["macro"] + vertices = button["vertices"] + + object_type = "Button" + + attributes = [("ObjectType", object_type)] + + self._xml_start_tag("x:ClientData", attributes) + + # Write the x:Anchor element. + self._write_anchor(vertices) + + # Write the x:PrintObject element. + self._write_print_object() + + # Write the x:AutoFill element. + self._write_auto_fill() + + # Write the x:FmlaMacro element. + self._write_fmla_macro(macro) + + # Write the x:TextHAlign element. + self._write_text_halign() + + # Write the x:TextVAlign element. + self._write_text_valign() + + self._xml_end_tag("x:ClientData") + + def _write_move_with_cells(self): + # Write the <x:MoveWithCells> element. + self._xml_empty_tag("x:MoveWithCells") + + def _write_size_with_cells(self): + # Write the <x:SizeWithCells> element. + self._xml_empty_tag("x:SizeWithCells") + + def _write_visible(self): + # Write the <x:Visible> element. + self._xml_empty_tag("x:Visible") + + def _write_anchor(self, vertices): + # Write the <x:Anchor> element. + (col_start, row_start, x1, y1, col_end, row_end, x2, y2) = vertices[:8] + + strings = [col_start, x1, row_start, y1, col_end, x2, row_end, y2] + strings = [str(i) for i in strings] + + data = ", ".join(strings) + + self._xml_data_element("x:Anchor", data) + + def _write_auto_fill(self): + # Write the <x:AutoFill> element. + data = "False" + + self._xml_data_element("x:AutoFill", data) + + def _write_row(self, data): + # Write the <x:Row> element. + self._xml_data_element("x:Row", data) + + def _write_column(self, data): + # Write the <x:Column> element. + self._xml_data_element("x:Column", data) + + def _write_print_object(self): + # Write the <x:PrintObject> element. + self._xml_data_element("x:PrintObject", "False") + + def _write_text_halign(self): + # Write the <x:TextHAlign> element. + self._xml_data_element("x:TextHAlign", "Center") + + def _write_text_valign(self): + # Write the <x:TextVAlign> element. + self._xml_data_element("x:TextVAlign", "Center") + + def _write_fmla_macro(self, data): + # Write the <x:FmlaMacro> element. + self._xml_data_element("x:FmlaMacro", data) + + def _write_imagedata(self, ref_id, o_title): + # Write the <v:imagedata> element. + attributes = [ + ("o:relid", "rId" + str(ref_id)), + ("o:title", o_title), + ] + + self._xml_empty_tag("v:imagedata", attributes) + + def _write_formulas(self): + # Write the <v:formulas> element. + self._xml_start_tag("v:formulas") + + # Write the v:f elements. + self._write_formula("if lineDrawn pixelLineWidth 0") + self._write_formula("sum @0 1 0") + self._write_formula("sum 0 0 @1") + self._write_formula("prod @2 1 2") + self._write_formula("prod @3 21600 pixelWidth") + self._write_formula("prod @3 21600 pixelHeight") + self._write_formula("sum @0 0 1") + self._write_formula("prod @6 1 2") + self._write_formula("prod @7 21600 pixelWidth") + self._write_formula("sum @8 21600 0") + self._write_formula("prod @7 21600 pixelHeight") + self._write_formula("sum @10 21600 0") + + self._xml_end_tag("v:formulas") + + def _write_formula(self, eqn): + # Write the <v:f> element. + attributes = [("eqn", eqn)] + + self._xml_empty_tag("v:f", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/workbook.py b/.venv/lib/python3.12/site-packages/xlsxwriter/workbook.py new file mode 100644 index 00000000..fc6aa5a3 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/workbook.py @@ -0,0 +1,1856 @@ +############################################################################### +# +# Workbook - A class for writing the Excel XLSX Workbook file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# Standard packages. +import operator +import os +import re +import time +from datetime import datetime, timezone +from decimal import Decimal +from fractions import Fraction +from warnings import warn +from zipfile import ZIP_DEFLATED, LargeZipFile, ZipFile, ZipInfo + +# Package imports. +from . import xmlwriter +from .chart_area import ChartArea +from .chart_bar import ChartBar +from .chart_column import ChartColumn +from .chart_doughnut import ChartDoughnut +from .chart_line import ChartLine +from .chart_pie import ChartPie +from .chart_radar import ChartRadar +from .chart_scatter import ChartScatter +from .chart_stock import ChartStock +from .chartsheet import Chartsheet +from .exceptions import ( + DuplicateWorksheetName, + FileCreateError, + FileSizeError, + InvalidWorksheetName, +) +from .format import Format +from .packager import Packager +from .sharedstrings import SharedStringTable +from .utility import _get_image_properties, xl_cell_to_rowcol +from .worksheet import Worksheet + + +class Workbook(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX Workbook file. + + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + chartsheet_class = Chartsheet + worksheet_class = Worksheet + + def __init__(self, filename=None, options=None): + """ + Constructor. + + """ + if options is None: + options = {} + + super().__init__() + + self.filename = filename + + self.tmpdir = options.get("tmpdir", None) + self.date_1904 = options.get("date_1904", False) + self.strings_to_numbers = options.get("strings_to_numbers", False) + self.strings_to_formulas = options.get("strings_to_formulas", True) + self.strings_to_urls = options.get("strings_to_urls", True) + self.nan_inf_to_errors = options.get("nan_inf_to_errors", False) + self.default_date_format = options.get("default_date_format", None) + self.constant_memory = options.get("constant_memory", False) + self.in_memory = options.get("in_memory", False) + self.excel2003_style = options.get("excel2003_style", False) + self.remove_timezone = options.get("remove_timezone", False) + self.use_future_functions = options.get("use_future_functions", False) + self.default_format_properties = options.get("default_format_properties", {}) + + self.max_url_length = options.get("max_url_length", 2079) + if self.max_url_length < 255: + self.max_url_length = 2079 + + if options.get("use_zip64"): + self.allow_zip64 = True + else: + self.allow_zip64 = False + + self.worksheet_meta = WorksheetMeta() + self.selected = 0 + self.fileclosed = 0 + self.filehandle = None + self.internal_fh = 0 + self.sheet_name = "Sheet" + self.chart_name = "Chart" + self.sheetname_count = 0 + self.chartname_count = 0 + self.worksheets_objs = [] + self.charts = [] + self.drawings = [] + self.sheetnames = {} + self.formats = [] + self.xf_formats = [] + self.xf_format_indices = {} + self.dxf_formats = [] + self.dxf_format_indices = {} + self.palette = [] + self.font_count = 0 + self.num_formats = [] + self.defined_names = [] + self.named_ranges = [] + self.custom_colors = [] + self.doc_properties = {} + self.custom_properties = [] + self.createtime = datetime.now(timezone.utc) + self.num_vml_files = 0 + self.num_comment_files = 0 + self.x_window = 240 + self.y_window = 15 + self.window_width = 16095 + self.window_height = 9660 + self.tab_ratio = 600 + self.str_table = SharedStringTable() + self.vba_project = None + self.vba_project_is_stream = False + self.vba_project_signature = None + self.vba_project_signature_is_stream = False + self.vba_codename = None + self.image_types = {} + self.images = [] + self.border_count = 0 + self.fill_count = 0 + self.drawing_count = 0 + self.calc_mode = "auto" + self.calc_on_load = True + self.calc_id = 124519 + self.has_comments = False + self.read_only = 0 + self.has_metadata = False + self.has_embedded_images = False + self.has_dynamic_functions = False + self.has_embedded_descriptions = False + self.embedded_images = EmbeddedImages() + self.feature_property_bags = set() + + # We can't do 'constant_memory' mode while doing 'in_memory' mode. + if self.in_memory: + self.constant_memory = False + + # Add the default cell format. + if self.excel2003_style: + self.add_format({"xf_index": 0, "font_family": 0}) + else: + self.add_format({"xf_index": 0}) + + # Add a default URL format. + self.default_url_format = self.add_format({"hyperlink": True}) + + # Add the default date format. + if self.default_date_format is not None: + self.default_date_format = self.add_format( + {"num_format": self.default_date_format} + ) + + def __enter__(self): + """Return self object to use with "with" statement.""" + return self + + def __exit__(self, type, value, traceback): + # pylint: disable=redefined-builtin + """Close workbook when exiting "with" statement.""" + self.close() + + def add_worksheet(self, name=None, worksheet_class=None): + """ + Add a new worksheet to the Excel workbook. + + Args: + name: The worksheet name. Defaults to 'Sheet1', etc. + + Returns: + Reference to a worksheet object. + + """ + if worksheet_class is None: + worksheet_class = self.worksheet_class + + return self._add_sheet(name, worksheet_class=worksheet_class) + + def add_chartsheet(self, name=None, chartsheet_class=None): + """ + Add a new chartsheet to the Excel workbook. + + Args: + name: The chartsheet name. Defaults to 'Sheet1', etc. + + Returns: + Reference to a chartsheet object. + + """ + if chartsheet_class is None: + chartsheet_class = self.chartsheet_class + + return self._add_sheet(name, worksheet_class=chartsheet_class) + + def add_format(self, properties=None): + """ + Add a new Format to the Excel Workbook. + + Args: + properties: The format properties. + + Returns: + Reference to a Format object. + + """ + format_properties = self.default_format_properties.copy() + + if self.excel2003_style: + format_properties = {"font_name": "Arial", "font_size": 10, "theme": 1 * -1} + + if properties: + format_properties.update(properties) + + xf_format = Format( + format_properties, self.xf_format_indices, self.dxf_format_indices + ) + + # Store the format reference. + self.formats.append(xf_format) + + return xf_format + + def add_chart(self, options): + """ + Create a chart object. + + Args: + options: The chart type and subtype options. + + Returns: + Reference to a Chart object. + + """ + + # Type must be specified so we can create the required chart instance. + chart_type = options.get("type") + if chart_type is None: + warn("Chart type must be defined in add_chart()") + return None + + if chart_type == "area": + chart = ChartArea(options) + elif chart_type == "bar": + chart = ChartBar(options) + elif chart_type == "column": + chart = ChartColumn(options) + elif chart_type == "doughnut": + chart = ChartDoughnut() + elif chart_type == "line": + chart = ChartLine(options) + elif chart_type == "pie": + chart = ChartPie() + elif chart_type == "radar": + chart = ChartRadar(options) + elif chart_type == "scatter": + chart = ChartScatter(options) + elif chart_type == "stock": + chart = ChartStock() + else: + warn(f"Unknown chart type '{chart_type}' in add_chart()") + return None + + # Set the embedded chart name if present. + if "name" in options: + chart.chart_name = options["name"] + + chart.embedded = True + chart.date_1904 = self.date_1904 + chart.remove_timezone = self.remove_timezone + + self.charts.append(chart) + + return chart + + def add_vba_project(self, vba_project, is_stream=False): + """ + Add a vbaProject binary to the Excel workbook. + + Args: + vba_project: The vbaProject binary file name. + is_stream: vba_project is an in memory byte stream. + + Returns: + 0 on success. + + """ + if not is_stream and not os.path.exists(vba_project): + warn(f"VBA project binary file '{vba_project}' not found.") + return -1 + + if self.vba_codename is None: + self.vba_codename = "ThisWorkbook" + + self.vba_project = vba_project + self.vba_project_is_stream = is_stream + + return 0 + + def add_signed_vba_project( + self, vba_project, signature, project_is_stream=False, signature_is_stream=False + ): + """ + Add a vbaProject binary and a vbaProjectSignature binary to the + Excel workbook. + + Args: + vba_project: The vbaProject binary file name. + signature: The vbaProjectSignature binary file name. + project_is_stream: vba_project is an in memory byte stream. + signature_is_stream: signature is an in memory byte stream. + + Returns: + 0 on success. + + """ + if self.add_vba_project(vba_project, project_is_stream) == -1: + return -1 + + if not signature_is_stream and not os.path.exists(signature): + warn(f"VBA project signature binary file '{signature}' not found.") + return -1 + + self.vba_project_signature = signature + self.vba_project_signature_is_stream = signature_is_stream + + return 0 + + def close(self): + """ + Call finalization code and close file. + + Args: + None. + + Returns: + Nothing. + + """ + # pylint: disable=raise-missing-from + if not self.fileclosed: + try: + self._store_workbook() + except IOError as e: + raise FileCreateError(e) + except LargeZipFile: + raise FileSizeError( + "Filesize would require ZIP64 extensions. " + "Use workbook.use_zip64()." + ) + + self.fileclosed = True + + # Ensure all constant_memory temp files are closed. + if self.constant_memory: + for worksheet in self.worksheets(): + worksheet._opt_close() + + else: + warn("Calling close() on already closed file.") + + def set_size(self, width, height): + """ + Set the size of a workbook window. + + Args: + width: Width of the window in pixels. + height: Height of the window in pixels. + + Returns: + Nothing. + + """ + # Convert the width/height to twips at 96 dpi. + if width: + self.window_width = int(width * 1440 / 96) + else: + self.window_width = 16095 + + if height: + self.window_height = int(height * 1440 / 96) + else: + self.window_height = 9660 + + def set_tab_ratio(self, tab_ratio=None): + """ + Set the ratio between worksheet tabs and the horizontal slider. + + Args: + tab_ratio: The tab ratio, 0 <= tab_ratio <= 100 + + Returns: + Nothing. + + """ + if tab_ratio is None: + return + + if tab_ratio < 0 or tab_ratio > 100: + warn(f"Tab ratio '{tab_ratio}' outside: 0 <= tab_ratio <= 100") + else: + self.tab_ratio = int(tab_ratio * 10) + + def set_properties(self, properties): + """ + Set the document properties such as Title, Author etc. + + Args: + properties: Dictionary of document properties. + + Returns: + Nothing. + + """ + self.doc_properties = properties + + def set_custom_property(self, name, value, property_type=None): + """ + Set a custom document property. + + Args: + name: The name of the custom property. + value: The value of the custom property. + property_type: The type of the custom property. Optional. + + Returns: + 0 on success. + + """ + if name is None or value is None: + warn( + "The name and value parameters must be non-None in " + "set_custom_property()" + ) + return -1 + + if property_type is None: + # Determine the property type from the Python type. + if isinstance(value, bool): + property_type = "bool" + elif isinstance(value, datetime): + property_type = "date" + elif isinstance(value, int): + property_type = "number_int" + elif isinstance(value, (float, int, Decimal, Fraction)): + property_type = "number" + else: + property_type = "text" + + if property_type == "date": + value = value.strftime("%Y-%m-%dT%H:%M:%SZ") + + if property_type == "text" and len(value) > 255: + warn( + f"Length of 'value' parameter exceeds Excel's limit of 255 " + f"characters in set_custom_property(): '{value}'" + ) + + if len(name) > 255: + warn( + f"Length of 'name' parameter exceeds Excel's limit of 255 " + f"characters in set_custom_property(): '{name}'" + ) + + self.custom_properties.append((name, value, property_type)) + + return 0 + + def set_calc_mode(self, mode, calc_id=None): + """ + Set the Excel calculation mode for the workbook. + + Args: + mode: String containing one of: + * manual + * auto_except_tables + * auto + + Returns: + Nothing. + + """ + self.calc_mode = mode + + if mode == "manual": + self.calc_on_load = False + elif mode == "auto_except_tables": + self.calc_mode = "autoNoTable" + + # Leave undocumented for now. Rarely required. + if calc_id: + self.calc_id = calc_id + + def define_name(self, name, formula): + # Create a defined name in Excel. We handle global/workbook level + # names and local/worksheet names. + """ + Create a defined name in the workbook. + + Args: + name: The defined name. + formula: The cell or range that the defined name refers to. + + Returns: + 0 on success. + + """ + sheet_index = None + sheetname = "" + + # Remove the = sign from the formula if it exists. + if formula.startswith("="): + formula = formula.lstrip("=") + + # Local defined names are formatted like "Sheet1!name". + sheet_parts = re.compile(r"^([^!]+)!([^!]+)$") + match = sheet_parts.match(name) + + if match: + sheetname = match.group(1) + name = match.group(2) + sheet_index = self._get_sheet_index(sheetname) + + # Warn if the sheet index wasn't found. + if sheet_index is None: + warn(f"Unknown sheet name '{sheetname}' in defined_name()") + return -1 + else: + # Use -1 to indicate global names. + sheet_index = -1 + + # Warn if the defined name contains invalid chars as defined by Excel. + if not re.match(r"^[\w\\][\w\\.]*$", name, re.UNICODE) or re.match( + r"^\d", name + ): + warn(f"Invalid Excel characters in defined_name(): '{name}'") + return -1 + + # Warn if the defined name looks like a cell name. + if re.match(r"^[a-zA-Z][a-zA-Z]?[a-dA-D]?\d+$", name): + warn(f"Name looks like a cell name in defined_name(): '{name}'") + return -1 + + # Warn if the name looks like a R1C1 cell reference. + if re.match(r"^[rcRC]$", name) or re.match(r"^[rcRC]\d+[rcRC]\d+$", name): + warn(f"Invalid name '{name}' like a RC cell ref in defined_name()") + return -1 + + self.defined_names.append([name, sheet_index, formula, False]) + + return 0 + + def worksheets(self): + """ + Return a list of the worksheet objects in the workbook. + + Args: + None. + + Returns: + A list of worksheet objects. + + """ + return self.worksheets_objs + + def get_worksheet_by_name(self, name): + """ + Return a worksheet object in the workbook using the sheetname. + + Args: + name: The name of the worksheet. + + Returns: + A worksheet object or None. + + """ + return self.sheetnames.get(name) + + def get_default_url_format(self): + """ + Get the default url format used when a user defined format isn't + specified with write_url(). The format is the hyperlink style defined + by Excel for the default theme. + + Args: + None. + + Returns: + A format object. + + """ + return self.default_url_format + + def use_zip64(self): + """ + Allow ZIP64 extensions when writing xlsx file zip container. + + Args: + None. + + Returns: + Nothing. + + """ + self.allow_zip64 = True + + def set_vba_name(self, name=None): + """ + Set the VBA name for the workbook. By default the workbook is referred + to as ThisWorkbook in VBA. + + Args: + name: The VBA name for the workbook. + + Returns: + Nothing. + + """ + if name is not None: + self.vba_codename = name + else: + self.vba_codename = "ThisWorkbook" + + def read_only_recommended(self): + """ + Set the Excel "Read-only recommended" option when saving a file. + + Args: + None. + + Returns: + Nothing. + + """ + self.read_only = 2 + + ########################################################################### + # + # Private API. + # + ########################################################################### + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Prepare format object for passing to Style.pm. + self._prepare_format_properties() + + # Write the XML declaration. + self._xml_declaration() + + # Write the workbook element. + self._write_workbook() + + # Write the fileVersion element. + self._write_file_version() + + # Write the fileSharing element. + self._write_file_sharing() + + # Write the workbookPr element. + self._write_workbook_pr() + + # Write the bookViews element. + self._write_book_views() + + # Write the sheets element. + self._write_sheets() + + # Write the workbook defined names. + self._write_defined_names() + + # Write the calcPr element. + self._write_calc_pr() + + # Close the workbook tag. + self._xml_end_tag("workbook") + + # Close the file. + self._xml_close() + + def _store_workbook(self): + # pylint: disable=consider-using-with + # Create the xlsx/zip file. + try: + xlsx_file = ZipFile( + self.filename, + "w", + compression=ZIP_DEFLATED, + allowZip64=self.allow_zip64, + ) + except IOError as e: + raise e + + # Assemble worksheets into a workbook. + packager = self._get_packager() + + # Add a default worksheet if non have been added. + if not self.worksheets(): + self.add_worksheet() + + # Ensure that at least one worksheet has been selected. + if self.worksheet_meta.activesheet == 0: + self.worksheets_objs[0].selected = 1 + self.worksheets_objs[0].hidden = 0 + + # Set the active sheet. + for sheet in self.worksheets(): + if sheet.index == self.worksheet_meta.activesheet: + sheet.active = 1 + + # Set the sheet vba_codename the workbook has a vbaProject binary. + if self.vba_project: + for sheet in self.worksheets(): + if sheet.vba_codename is None: + sheet.set_vba_name() + + # Convert the SST strings data structure. + self._prepare_sst_string_data() + + # Prepare the worksheet VML elements such as comments and buttons. + self._prepare_vml() + + # Set the defined names for the worksheets such as Print Titles. + self._prepare_defined_names() + + # Prepare the drawings, charts and images. + self._prepare_drawings() + + # Add cached data to charts. + self._add_chart_data() + + # Prepare the worksheet tables. + self._prepare_tables() + + # Prepare the metadata file links. + self._prepare_metadata() + + # Package the workbook. + packager._add_workbook(self) + packager._set_tmpdir(self.tmpdir) + packager._set_in_memory(self.in_memory) + xml_files = packager._create_package() + + # Free up the Packager object. + packager = None + + # Add XML sub-files to the Zip file with their Excel filename. + for file_id, file_data in enumerate(xml_files): + os_filename, xml_filename, is_binary = file_data + + if self.in_memory: + # Set sub-file timestamp to Excel's timestamp of 1/1/1980. + zipinfo = ZipInfo(xml_filename, (1980, 1, 1, 0, 0, 0)) + + # Copy compression type from parent ZipFile. + zipinfo.compress_type = xlsx_file.compression + + if is_binary: + xlsx_file.writestr(zipinfo, os_filename.getvalue()) + else: + xlsx_file.writestr(zipinfo, os_filename.getvalue().encode("utf-8")) + else: + # The sub-files are tempfiles on disk, i.e, not in memory. + + # Set sub-file timestamp to 31/1/1980 due to portability + # issues setting it to Excel's timestamp of 1/1/1980. + timestamp = time.mktime((1980, 1, 31, 0, 0, 0, 0, 0, -1)) + os.utime(os_filename, (timestamp, timestamp)) + + try: + xlsx_file.write(os_filename, xml_filename) + os.remove(os_filename) + except LargeZipFile as e: + # Close open temp files on zipfile.LargeZipFile exception. + for i in range(file_id, len(xml_files) - 1): + os.remove(xml_files[i][0]) + raise e + + xlsx_file.close() + + def _add_sheet(self, name, worksheet_class=None): + # Utility for shared code in add_worksheet() and add_chartsheet(). + + if worksheet_class: + worksheet = worksheet_class() + else: + worksheet = self.worksheet_class() + + sheet_index = len(self.worksheets_objs) + name = self._check_sheetname(name, isinstance(worksheet, Chartsheet)) + + # Initialization data to pass to the worksheet. + init_data = { + "name": name, + "index": sheet_index, + "str_table": self.str_table, + "worksheet_meta": self.worksheet_meta, + "constant_memory": self.constant_memory, + "tmpdir": self.tmpdir, + "date_1904": self.date_1904, + "strings_to_numbers": self.strings_to_numbers, + "strings_to_formulas": self.strings_to_formulas, + "strings_to_urls": self.strings_to_urls, + "nan_inf_to_errors": self.nan_inf_to_errors, + "default_date_format": self.default_date_format, + "default_url_format": self.default_url_format, + "workbook_add_format": self.add_format, + "excel2003_style": self.excel2003_style, + "remove_timezone": self.remove_timezone, + "max_url_length": self.max_url_length, + "use_future_functions": self.use_future_functions, + "embedded_images": self.embedded_images, + } + + worksheet._initialize(init_data) + + self.worksheets_objs.append(worksheet) + self.sheetnames[name] = worksheet + + return worksheet + + def _check_sheetname(self, sheetname, is_chartsheet=False): + # Check for valid worksheet names. We check the length, if it contains + # any invalid chars and if the sheetname is unique in the workbook. + invalid_char = re.compile(r"[\[\]:*?/\\]") + + # Increment the Sheet/Chart number used for default sheet names below. + if is_chartsheet: + self.chartname_count += 1 + else: + self.sheetname_count += 1 + + # Supply default Sheet/Chart sheetname if none has been defined. + if sheetname is None or sheetname == "": + if is_chartsheet: + sheetname = self.chart_name + str(self.chartname_count) + else: + sheetname = self.sheet_name + str(self.sheetname_count) + + # Check that sheet sheetname is <= 31. Excel limit. + if len(sheetname) > 31: + raise InvalidWorksheetName( + f"Excel worksheet name '{sheetname}' must be <= 31 chars." + ) + + # Check that sheetname doesn't contain any invalid characters. + if invalid_char.search(sheetname): + raise InvalidWorksheetName( + f"Invalid Excel character '[]:*?/\\' in sheetname '{sheetname}'." + ) + + # Check that sheetname doesn't start or end with an apostrophe. + if sheetname.startswith("'") or sheetname.endswith("'"): + raise InvalidWorksheetName( + f'Sheet name cannot start or end with an apostrophe "{sheetname}".' + ) + + # Check that the worksheet name doesn't already exist since this is a + # fatal Excel error. The check must be case insensitive like Excel. + for worksheet in self.worksheets(): + if sheetname.lower() == worksheet.name.lower(): + raise DuplicateWorksheetName( + f"Sheetname '{sheetname}', with case ignored, is already in use." + ) + + return sheetname + + def _prepare_format_properties(self): + # Prepare all Format properties prior to passing them to styles.py. + + # Separate format objects into XF and DXF formats. + self._prepare_formats() + + # Set the font index for the format objects. + self._prepare_fonts() + + # Set the number format index for the format objects. + self._prepare_num_formats() + + # Set the border index for the format objects. + self._prepare_borders() + + # Set the fill index for the format objects. + self._prepare_fills() + + def _prepare_formats(self): + # Iterate through the XF Format objects and separate them into + # XF and DXF formats. The XF and DF formats then need to be sorted + # back into index order rather than creation order. + xf_formats = [] + dxf_formats = [] + + # Sort into XF and DXF formats. + for xf_format in self.formats: + if xf_format.xf_index is not None: + xf_formats.append(xf_format) + + if xf_format.dxf_index is not None: + dxf_formats.append(xf_format) + + # Pre-extend the format lists. + self.xf_formats = [None] * len(xf_formats) + self.dxf_formats = [None] * len(dxf_formats) + + # Rearrange formats into index order. + for xf_format in xf_formats: + index = xf_format.xf_index + self.xf_formats[index] = xf_format + + for dxf_format in dxf_formats: + index = dxf_format.dxf_index + self.dxf_formats[index] = dxf_format + + def _set_default_xf_indices(self): + # Set the default index for each format. Only used for testing. + + formats = list(self.formats) + + # Delete the default url format. + del formats[1] + + # Skip the default date format if set. + if self.default_date_format is not None: + del formats[1] + + # Set the remaining formats. + for xf_format in formats: + xf_format._get_xf_index() + + def _prepare_fonts(self): + # Iterate through the XF Format objects and give them an index to + # non-default font elements. + fonts = {} + index = 0 + + for xf_format in self.xf_formats: + key = xf_format._get_font_key() + if key in fonts: + # Font has already been used. + xf_format.font_index = fonts[key] + xf_format.has_font = 0 + else: + # This is a new font. + fonts[key] = index + xf_format.font_index = index + xf_format.has_font = 1 + index += 1 + + self.font_count = index + + # For DXF formats we only need to check if the properties have changed. + for xf_format in self.dxf_formats: + # The only font properties that can change for a DXF format are: + # color, bold, italic, underline and strikethrough. + if ( + xf_format.font_color + or xf_format.bold + or xf_format.italic + or xf_format.underline + or xf_format.font_strikeout + ): + xf_format.has_dxf_font = 1 + + def _prepare_num_formats(self): + # User defined records in Excel start from index 0xA4. + unique_num_formats = {} + num_formats = [] + index = 164 + + for xf_format in self.xf_formats + self.dxf_formats: + num_format = xf_format.num_format + + # Check if num_format is an index to a built-in number format. + if not isinstance(num_format, str): + num_format = int(num_format) + + # Number format '0' is indexed as 1 in Excel. + if num_format == 0: + num_format = 1 + + xf_format.num_format_index = num_format + continue + + if num_format == "0": + # Number format '0' is indexed as 1 in Excel. + xf_format.num_format_index = 1 + continue + + if num_format == "General": + # The 'General' format has an number format index of 0. + xf_format.num_format_index = 0 + continue + + if num_format in unique_num_formats: + # Number xf_format has already been used. + xf_format.num_format_index = unique_num_formats[num_format] + else: + # Add a new number xf_format. + unique_num_formats[num_format] = index + xf_format.num_format_index = index + index += 1 + + # Only increase font count for XF formats (not DXF formats). + if xf_format.xf_index: + num_formats.append(num_format) + + self.num_formats = num_formats + + def _prepare_borders(self): + # Iterate through the XF Format objects and give them an index to + # non-default border elements. + borders = {} + index = 0 + + for xf_format in self.xf_formats: + key = xf_format._get_border_key() + + if key in borders: + # Border has already been used. + xf_format.border_index = borders[key] + xf_format.has_border = 0 + else: + # This is a new border. + borders[key] = index + xf_format.border_index = index + xf_format.has_border = 1 + index += 1 + + self.border_count = index + + # For DXF formats we only need to check if the properties have changed. + has_border = re.compile(r"[^0:]") + + for xf_format in self.dxf_formats: + key = xf_format._get_border_key() + + if has_border.search(key): + xf_format.has_dxf_border = 1 + + def _prepare_fills(self): + # Iterate through the XF Format objects and give them an index to + # non-default fill elements. + # The user defined fill properties start from 2 since there are 2 + # default fills: patternType="none" and patternType="gray125". + fills = {} + index = 2 # Start from 2. See above. + + # Add the default fills. + fills["0:0:0"] = 0 + fills["17:0:0"] = 1 + + # Store the DXF colors separately since them may be reversed below. + for xf_format in self.dxf_formats: + if xf_format.pattern or xf_format.bg_color or xf_format.fg_color: + xf_format.has_dxf_fill = 1 + xf_format.dxf_bg_color = xf_format.bg_color + xf_format.dxf_fg_color = xf_format.fg_color + + for xf_format in self.xf_formats: + # The following logical statements jointly take care of special + # cases in relation to cell colors and patterns: + # 1. For a solid fill (_pattern == 1) Excel reverses the role of + # foreground and background colors, and + # 2. If the user specifies a foreground or background color + # without a pattern they probably wanted a solid fill, so we fill + # in the defaults. + if ( + xf_format.pattern == 1 + and xf_format.bg_color != 0 + and xf_format.fg_color != 0 + ): + tmp = xf_format.fg_color + xf_format.fg_color = xf_format.bg_color + xf_format.bg_color = tmp + + if ( + xf_format.pattern <= 1 + and xf_format.bg_color != 0 + and xf_format.fg_color == 0 + ): + xf_format.fg_color = xf_format.bg_color + xf_format.bg_color = 0 + xf_format.pattern = 1 + + if ( + xf_format.pattern <= 1 + and xf_format.bg_color == 0 + and xf_format.fg_color != 0 + ): + xf_format.pattern = 1 + + key = xf_format._get_fill_key() + + if key in fills: + # Fill has already been used. + xf_format.fill_index = fills[key] + xf_format.has_fill = 0 + else: + # This is a new fill. + fills[key] = index + xf_format.fill_index = index + xf_format.has_fill = 1 + index += 1 + + self.fill_count = index + + def _has_feature_property_bags(self): + # Check for any format properties that require a feature bag. Currently + # this only applies to checkboxes. + if not self.feature_property_bags: + for xf_format in self.formats: + if xf_format.checkbox: + self.feature_property_bags.add("XFComplements") + + if xf_format.dxf_index is not None and xf_format.checkbox: + self.feature_property_bags.add("DXFComplements") + + return self.feature_property_bags + + def _prepare_defined_names(self): + # Iterate through the worksheets and store any defined names in + # addition to any user defined names. Stores the defined names + # for the Workbook.xml and the named ranges for App.xml. + defined_names = self.defined_names + + for sheet in self.worksheets(): + # Check for Print Area settings. + if sheet.autofilter_area: + hidden = 1 + sheet_range = sheet.autofilter_area + # Store the defined names. + defined_names.append( + ["_xlnm._FilterDatabase", sheet.index, sheet_range, hidden] + ) + + # Check for Print Area settings. + if sheet.print_area_range: + hidden = 0 + sheet_range = sheet.print_area_range + # Store the defined names. + defined_names.append( + ["_xlnm.Print_Area", sheet.index, sheet_range, hidden] + ) + + # Check for repeat rows/cols referred to as Print Titles. + if sheet.repeat_col_range or sheet.repeat_row_range: + hidden = 0 + sheet_range = "" + if sheet.repeat_col_range and sheet.repeat_row_range: + sheet_range = sheet.repeat_col_range + "," + sheet.repeat_row_range + else: + sheet_range = sheet.repeat_col_range + sheet.repeat_row_range + # Store the defined names. + defined_names.append( + ["_xlnm.Print_Titles", sheet.index, sheet_range, hidden] + ) + + defined_names = self._sort_defined_names(defined_names) + self.defined_names = defined_names + self.named_ranges = self._extract_named_ranges(defined_names) + + def _sort_defined_names(self, names): + # Sort the list of list of internal and user defined names in + # the same order as used by Excel. + + # Add a normalize name string to each list for sorting. + for name_list in names: + (defined_name, _, sheet_name, _) = name_list + + # Normalize the defined name by removing any leading '_xmln.' + # from internal names and lowercasing the string. + defined_name = defined_name.replace("_xlnm.", "").lower() + + # Normalize the sheetname by removing the leading quote and + # lowercasing the string. + sheet_name = sheet_name.lstrip("'").lower() + + name_list.append(defined_name + "::" + sheet_name) + + # Sort based on the normalized key. + names.sort(key=operator.itemgetter(4)) + + # Remove the extra key used for sorting. + for name_list in names: + name_list.pop() + + return names + + def _prepare_drawings(self): + # Iterate through the worksheets and set up chart and image drawings. + chart_ref_id = 0 + ref_id = 0 + drawing_id = 0 + image_ids = {} + header_image_ids = {} + background_ids = {} + + # Store the image types for any embedded images. + for image_data in self.embedded_images.images: + image_type = image_data[1] + self.image_types[image_type] = True + if image_data[3]: + self.has_embedded_descriptions = True + + image_ref_id = len(self.embedded_images.images) + + for sheet in self.worksheets(): + chart_count = len(sheet.charts) + image_count = len(sheet.images) + shape_count = len(sheet.shapes) + + header_image_count = len(sheet.header_images) + footer_image_count = len(sheet.footer_images) + has_background = sheet.background_image + has_drawing = False + + if not ( + chart_count + or image_count + or shape_count + or header_image_count + or footer_image_count + or has_background + ): + continue + + # Don't increase the drawing_id header/footer images. + if chart_count or image_count or shape_count: + drawing_id += 1 + has_drawing = True + + # Prepare the background images. + if sheet.background_image: + if sheet.background_bytes: + filename = "" + image_data = sheet.background_image + else: + filename = sheet.background_image + image_data = None + + ( + image_type, + _, + _, + _, + _, + _, + digest, + ) = _get_image_properties(filename, image_data) + + self.image_types[image_type] = True + + if digest in background_ids: + ref_id = background_ids[digest] + else: + image_ref_id += 1 + ref_id = image_ref_id + background_ids[digest] = image_ref_id + self.images.append([filename, image_type, image_data]) + + sheet._prepare_background(ref_id, image_type) + + # Prepare the worksheet images. + for index in range(image_count): + filename = sheet.images[index][2] + image_data = sheet.images[index][10] + ( + image_type, + width, + height, + name, + x_dpi, + y_dpi, + digest, + ) = _get_image_properties(filename, image_data) + + self.image_types[image_type] = True + + if digest in image_ids: + ref_id = image_ids[digest] + else: + image_ref_id += 1 + ref_id = image_ref_id + image_ids[digest] = image_ref_id + self.images.append([filename, image_type, image_data]) + + sheet._prepare_image( + index, + ref_id, + drawing_id, + width, + height, + name, + image_type, + x_dpi, + y_dpi, + digest, + ) + + # Prepare the worksheet charts. + for index in range(chart_count): + chart_ref_id += 1 + sheet._prepare_chart(index, chart_ref_id, drawing_id) + + # Prepare the worksheet shapes. + for index in range(shape_count): + sheet._prepare_shape(index, drawing_id) + + # Prepare the header images. + for index in range(header_image_count): + filename = sheet.header_images[index][0] + image_data = sheet.header_images[index][1] + position = sheet.header_images[index][2] + + ( + image_type, + width, + height, + name, + x_dpi, + y_dpi, + digest, + ) = _get_image_properties(filename, image_data) + + self.image_types[image_type] = True + + if digest in header_image_ids: + ref_id = header_image_ids[digest] + else: + image_ref_id += 1 + ref_id = image_ref_id + header_image_ids[digest] = image_ref_id + self.images.append([filename, image_type, image_data]) + + sheet._prepare_header_image( + ref_id, + width, + height, + name, + image_type, + position, + x_dpi, + y_dpi, + digest, + ) + + # Prepare the footer images. + for index in range(footer_image_count): + filename = sheet.footer_images[index][0] + image_data = sheet.footer_images[index][1] + position = sheet.footer_images[index][2] + + ( + image_type, + width, + height, + name, + x_dpi, + y_dpi, + digest, + ) = _get_image_properties(filename, image_data) + + self.image_types[image_type] = True + + if digest in header_image_ids: + ref_id = header_image_ids[digest] + else: + image_ref_id += 1 + ref_id = image_ref_id + header_image_ids[digest] = image_ref_id + self.images.append([filename, image_type, image_data]) + + sheet._prepare_header_image( + ref_id, + width, + height, + name, + image_type, + position, + x_dpi, + y_dpi, + digest, + ) + + if has_drawing: + drawing = sheet.drawing + self.drawings.append(drawing) + + # Remove charts that were created but not inserted into worksheets. + for chart in self.charts[:]: + if chart.id == -1: + self.charts.remove(chart) + + # Sort the workbook charts references into the order that the were + # written to the worksheets above. + self.charts = sorted(self.charts, key=lambda chart: chart.id) + + self.drawing_count = drawing_id + + def _extract_named_ranges(self, defined_names): + # Extract the named ranges from the sorted list of defined names. + # These are used in the App.xml file. + named_ranges = [] + + for defined_name in defined_names: + name = defined_name[0] + index = defined_name[1] + sheet_range = defined_name[2] + + # Skip autoFilter ranges. + if name == "_xlnm._FilterDatabase": + continue + + # We are only interested in defined names with ranges. + if "!" in sheet_range: + sheet_name, _ = sheet_range.split("!", 1) + + # Match Print_Area and Print_Titles xlnm types. + if name.startswith("_xlnm."): + xlnm_type = name.replace("_xlnm.", "") + name = sheet_name + "!" + xlnm_type + elif index != -1: + name = sheet_name + "!" + name + + named_ranges.append(name) + + return named_ranges + + def _get_sheet_index(self, sheetname): + # Convert a sheet name to its index. Return None otherwise. + sheetname = sheetname.strip("'") + + if sheetname in self.sheetnames: + return self.sheetnames[sheetname].index + + return None + + def _prepare_vml(self): + # Iterate through the worksheets and set up the VML objects. + comment_id = 0 + vml_drawing_id = 0 + vml_data_id = 1 + vml_header_id = 0 + vml_shape_id = 1024 + vml_files = 0 + comment_files = 0 + + for sheet in self.worksheets(): + if not sheet.has_vml and not sheet.has_header_vml: + continue + + vml_files += 1 + + if sheet.has_vml: + if sheet.has_comments: + comment_files += 1 + comment_id += 1 + self.has_comments = True + + vml_drawing_id += 1 + + count = sheet._prepare_vml_objects( + vml_data_id, vml_shape_id, vml_drawing_id, comment_id + ) + + # Each VML should start with a shape id incremented by 1024. + vml_data_id += 1 * int((1024 + count) / 1024) + vml_shape_id += 1024 * int((1024 + count) / 1024) + + if sheet.has_header_vml: + vml_header_id += 1 + vml_drawing_id += 1 + sheet._prepare_header_vml_objects(vml_header_id, vml_drawing_id) + + self.num_vml_files = vml_files + self.num_comment_files = comment_files + + def _prepare_tables(self): + # Set the table ids for the worksheet tables. + table_id = 0 + seen = {} + + for sheet in self.worksheets(): + table_count = len(sheet.tables) + + if not table_count: + continue + + sheet._prepare_tables(table_id + 1, seen) + table_id += table_count + + def _prepare_metadata(self): + # Set the metadata rel link. + self.has_embedded_images = self.embedded_images.has_images() + self.has_metadata = self.has_embedded_images + + for sheet in self.worksheets(): + if sheet.has_dynamic_arrays: + self.has_metadata = True + self.has_dynamic_functions = True + + def _add_chart_data(self): + # Add "cached" data to charts to provide the numCache and strCache + # data for series and title/axis ranges. + worksheets = {} + seen_ranges = {} + charts = [] + + # Map worksheet names to worksheet objects. + for worksheet in self.worksheets(): + worksheets[worksheet.name] = worksheet + + # Build a list of the worksheet charts including any combined charts. + for chart in self.charts: + charts.append(chart) + if chart.combined: + charts.append(chart.combined) + + for chart in charts: + for c_range in chart.formula_ids.keys(): + r_id = chart.formula_ids[c_range] + + # Skip if the series has user defined data. + if chart.formula_data[r_id] is not None: + if c_range not in seen_ranges or seen_ranges[c_range] is None: + data = chart.formula_data[r_id] + seen_ranges[c_range] = data + continue + + # Check to see if the data is already cached locally. + if c_range in seen_ranges: + chart.formula_data[r_id] = seen_ranges[c_range] + continue + + # Convert the range formula to a sheet name and cell range. + (sheetname, cells) = self._get_chart_range(c_range) + + # Skip if we couldn't parse the formula. + if sheetname is None: + continue + + # Handle non-contiguous ranges like: + # (Sheet1!$A$1:$A$2,Sheet1!$A$4:$A$5). + # We don't try to parse them. We just return an empty list. + if sheetname.startswith("("): + chart.formula_data[r_id] = [] + seen_ranges[c_range] = [] + continue + + # Warn if the name is unknown since it indicates a user error + # in a chart series formula. + if sheetname not in worksheets: + warn( + f"Unknown worksheet reference '{sheetname}' in range " + f"'{c_range}' passed to add_series()" + ) + chart.formula_data[r_id] = [] + seen_ranges[c_range] = [] + continue + + # Find the worksheet object based on the sheet name. + worksheet = worksheets[sheetname] + + # Get the data from the worksheet table. + data = worksheet._get_range_data(*cells) + + # Add the data to the chart. + chart.formula_data[r_id] = data + + # Store range data locally to avoid lookup if seen again. + seen_ranges[c_range] = data + + def _get_chart_range(self, c_range): + # Convert a range formula such as Sheet1!$B$1:$B$5 into a sheet name + # and cell range such as ( 'Sheet1', 0, 1, 4, 1 ). + + # Split the range formula into sheetname and cells at the last '!'. + pos = c_range.rfind("!") + if pos > 0: + sheetname = c_range[:pos] + cells = c_range[pos + 1 :] + else: + return None, None + + # Split the cell range into 2 cells or else use single cell for both. + if cells.find(":") > 0: + (cell_1, cell_2) = cells.split(":", 1) + else: + (cell_1, cell_2) = (cells, cells) + + # Remove leading/trailing quotes and convert escaped quotes to single. + sheetname = sheetname.strip("'") + sheetname = sheetname.replace("''", "'") + + try: + # Get the row, col values from the Excel ranges. We do this in a + # try block for ranges that can't be parsed such as defined names. + (row_start, col_start) = xl_cell_to_rowcol(cell_1) + (row_end, col_end) = xl_cell_to_rowcol(cell_2) + except AttributeError: + return None, None + + # We only handle 1D ranges. + if row_start != row_end and col_start != col_end: + return None, None + + return sheetname, [row_start, col_start, row_end, col_end] + + def _prepare_sst_string_data(self): + # Convert the SST string data from a dict to a list. + self.str_table._sort_string_data() + + def _get_packager(self): + # Get and instance of the Packager class to create the xlsx package. + # This allows the default packager to be over-ridden. + return Packager() + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_workbook(self): + # Write <workbook> element. + + schema = "http://schemas.openxmlformats.org" + xmlns = schema + "/spreadsheetml/2006/main" + xmlns_r = schema + "/officeDocument/2006/relationships" + + attributes = [ + ("xmlns", xmlns), + ("xmlns:r", xmlns_r), + ] + + self._xml_start_tag("workbook", attributes) + + def _write_file_version(self): + # Write the <fileVersion> element. + + app_name = "xl" + last_edited = 4 + lowest_edited = 4 + rup_build = 4505 + + attributes = [ + ("appName", app_name), + ("lastEdited", last_edited), + ("lowestEdited", lowest_edited), + ("rupBuild", rup_build), + ] + + if self.vba_project: + attributes.append(("codeName", "{37E998C4-C9E5-D4B9-71C8-EB1FF731991C}")) + + self._xml_empty_tag("fileVersion", attributes) + + def _write_file_sharing(self): + # Write the <fileSharing> element. + if self.read_only == 0: + return + + attributes = [("readOnlyRecommended", 1)] + + self._xml_empty_tag("fileSharing", attributes) + + def _write_workbook_pr(self): + # Write <workbookPr> element. + default_theme_version = 124226 + attributes = [] + + if self.vba_codename: + attributes.append(("codeName", self.vba_codename)) + if self.date_1904: + attributes.append(("date1904", 1)) + + attributes.append(("defaultThemeVersion", default_theme_version)) + + self._xml_empty_tag("workbookPr", attributes) + + def _write_book_views(self): + # Write <bookViews> element. + self._xml_start_tag("bookViews") + self._write_workbook_view() + self._xml_end_tag("bookViews") + + def _write_workbook_view(self): + # Write <workbookView> element. + attributes = [ + ("xWindow", self.x_window), + ("yWindow", self.y_window), + ("windowWidth", self.window_width), + ("windowHeight", self.window_height), + ] + + # Store the tabRatio attribute when it isn't the default. + if self.tab_ratio != 600: + attributes.append(("tabRatio", self.tab_ratio)) + + # Store the firstSheet attribute when it isn't the default. + if self.worksheet_meta.firstsheet > 0: + firstsheet = self.worksheet_meta.firstsheet + 1 + attributes.append(("firstSheet", firstsheet)) + + # Store the activeTab attribute when it isn't the first sheet. + if self.worksheet_meta.activesheet > 0: + attributes.append(("activeTab", self.worksheet_meta.activesheet)) + + self._xml_empty_tag("workbookView", attributes) + + def _write_sheets(self): + # Write <sheets> element. + self._xml_start_tag("sheets") + + id_num = 1 + for worksheet in self.worksheets(): + self._write_sheet(worksheet.name, id_num, worksheet.hidden) + id_num += 1 + + self._xml_end_tag("sheets") + + def _write_sheet(self, name, sheet_id, hidden): + # Write <sheet> element. + attributes = [ + ("name", name), + ("sheetId", sheet_id), + ] + + if hidden == 1: + attributes.append(("state", "hidden")) + elif hidden == 2: + attributes.append(("state", "veryHidden")) + + attributes.append(("r:id", "rId" + str(sheet_id))) + + self._xml_empty_tag("sheet", attributes) + + def _write_calc_pr(self): + # Write the <calcPr> element. + attributes = [("calcId", self.calc_id)] + + if self.calc_mode == "manual": + attributes.append(("calcMode", self.calc_mode)) + attributes.append(("calcOnSave", "0")) + elif self.calc_mode == "autoNoTable": + attributes.append(("calcMode", self.calc_mode)) + + if self.calc_on_load: + attributes.append(("fullCalcOnLoad", "1")) + + self._xml_empty_tag("calcPr", attributes) + + def _write_defined_names(self): + # Write the <definedNames> element. + if not self.defined_names: + return + + self._xml_start_tag("definedNames") + + for defined_name in self.defined_names: + self._write_defined_name(defined_name) + + self._xml_end_tag("definedNames") + + def _write_defined_name(self, defined_name): + # Write the <definedName> element. + name = defined_name[0] + sheet_id = defined_name[1] + sheet_range = defined_name[2] + hidden = defined_name[3] + + attributes = [("name", name)] + + if sheet_id != -1: + attributes.append(("localSheetId", sheet_id)) + if hidden: + attributes.append(("hidden", 1)) + + self._xml_data_element("definedName", sheet_range, attributes) + + +# A metadata class to share data between worksheets. +class WorksheetMeta: + """ + A class to track worksheets data such as the active sheet and the + first sheet. + + """ + + def __init__(self): + self.activesheet = 0 + self.firstsheet = 0 + + +# A helper class to share embedded images between worksheets. +class EmbeddedImages: + """ + A class to track duplicate embedded images between worksheets. + + """ + + def __init__(self): + self.images = [] + self.image_indexes = {} + + def get_image_index(self, image, digest): + """ + Get the index of an embedded image. + + Args: + image: The image to lookup. + digest: The digest of the image. + + Returns: + The image index. + + """ + image_index = self.image_indexes.get(digest) + + if image_index is None: + self.images.append(image) + image_index = len(self.images) + self.image_indexes[digest] = image_index + + return image_index + + def has_images(self): + """ + Check if the worksheet has embedded images. + + Args: + None. + + Returns: + Boolean. + + """ + return len(self.images) > 0 diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/worksheet.py b/.venv/lib/python3.12/site-packages/xlsxwriter/worksheet.py new file mode 100644 index 00000000..08b66032 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/worksheet.py @@ -0,0 +1,8554 @@ +############################################################################### +# +# Worksheet - A class for writing the Excel XLSX Worksheet file. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# pylint: disable=too-many-return-statements + +# Standard packages. +import datetime +import math +import os +import re +import tempfile +from collections import defaultdict, namedtuple +from decimal import Decimal +from fractions import Fraction +from functools import wraps +from io import StringIO +from math import isinf, isnan +from warnings import warn + +# Package imports. +from . import xmlwriter +from .drawing import Drawing +from .exceptions import DuplicateTableName, OverlappingRange +from .format import Format +from .shape import Shape +from .utility import ( + _datetime_to_excel_datetime, + _get_image_properties, + _get_sparkline_style, + _preserve_whitespace, + _supported_datetime, + _xl_color, + quote_sheetname, + xl_cell_to_rowcol, + xl_col_to_name, + xl_pixel_width, + xl_range, + xl_rowcol_to_cell, + xl_rowcol_to_cell_fast, +) +from .xmlwriter import XMLwriter + +re_dynamic_function = re.compile( + r""" + \bANCHORARRAY\( | + \bBYCOL\( | + \bBYROW\( | + \bCHOOSECOLS\( | + \bCHOOSEROWS\( | + \bDROP\( | + \bEXPAND\( | + \bFILTER\( | + \bHSTACK\( | + \bLAMBDA\( | + \bMAKEARRAY\( | + \bMAP\( | + \bRANDARRAY\( | + \bREDUCE\( | + \bSCAN\( | + \bSEQUENCE\( | + \bSINGLE\( | + \bSORT\( | + \bSORTBY\( | + \bSWITCH\( | + \bTAKE\( | + \bTEXTSPLIT\( | + \bTOCOL\( | + \bTOROW\( | + \bUNIQUE\( | + \bVSTACK\( | + \bWRAPCOLS\( | + \bWRAPROWS\( | + \bXLOOKUP\(""", + re.VERBOSE, +) + + +############################################################################### +# +# Decorator functions. +# +############################################################################### +def convert_cell_args(method): + """ + Decorator function to convert A1 notation in cell method calls + to the default row/col notation. + + """ + + @wraps(method) + def cell_wrapper(self, *args, **kwargs): + try: + # First arg is an int, default to row/col notation. + if args: + first_arg = args[0] + int(first_arg) + except ValueError: + # First arg isn't an int, convert to A1 notation. + new_args = xl_cell_to_rowcol(first_arg) + args = new_args + args[1:] + + return method(self, *args, **kwargs) + + return cell_wrapper + + +def convert_range_args(method): + """ + Decorator function to convert A1 notation in range method calls + to the default row/col notation. + + """ + + @wraps(method) + def cell_wrapper(self, *args, **kwargs): + try: + # First arg is an int, default to row/col notation. + if args: + int(args[0]) + except ValueError: + # First arg isn't an int, convert to A1 notation. + if ":" in args[0]: + cell_1, cell_2 = args[0].split(":") + row_1, col_1 = xl_cell_to_rowcol(cell_1) + row_2, col_2 = xl_cell_to_rowcol(cell_2) + else: + row_1, col_1 = xl_cell_to_rowcol(args[0]) + row_2, col_2 = row_1, col_1 + + new_args = [row_1, col_1, row_2, col_2] + new_args.extend(args[1:]) + args = new_args + + return method(self, *args, **kwargs) + + return cell_wrapper + + +def convert_column_args(method): + """ + Decorator function to convert A1 notation in columns method calls + to the default row/col notation. + + """ + + @wraps(method) + def column_wrapper(self, *args, **kwargs): + try: + # First arg is an int, default to row/col notation. + if args: + int(args[0]) + except ValueError: + # First arg isn't an int, convert to A1 notation. + cell_1, cell_2 = [col + "1" for col in args[0].split(":")] + _, col_1 = xl_cell_to_rowcol(cell_1) + _, col_2 = xl_cell_to_rowcol(cell_2) + new_args = [col_1, col_2] + new_args.extend(args[1:]) + args = new_args + + return method(self, *args, **kwargs) + + return column_wrapper + + +############################################################################### +# +# Named tuples used for cell types. +# +############################################################################### +CellBlankTuple = namedtuple("Blank", "format") +CellErrorTuple = namedtuple("Error", "error, format, value") +CellNumberTuple = namedtuple("Number", "number, format") +CellStringTuple = namedtuple("String", "string, format") +CellBooleanTuple = namedtuple("Boolean", "boolean, format") +CellFormulaTuple = namedtuple("Formula", "formula, format, value") +CellDatetimeTuple = namedtuple("Datetime", "number, format") +CellRichStringTuple = namedtuple("RichString", "string, format, raw_string") +CellArrayFormulaTuple = namedtuple( + "ArrayFormula", "formula, format, value, range, atype" +) + + +############################################################################### +# +# Worksheet Class definition. +# +############################################################################### +class Worksheet(xmlwriter.XMLwriter): + """ + A class for writing the Excel XLSX Worksheet file. + + """ + + ########################################################################### + # + # Public API. + # + ########################################################################### + + def __init__(self): + """ + Constructor. + + """ + + super().__init__() + + self.name = None + self.index = None + self.str_table = None + self.palette = None + self.constant_memory = 0 + self.tmpdir = None + self.is_chartsheet = False + + self.ext_sheets = [] + self.fileclosed = 0 + self.excel_version = 2007 + self.excel2003_style = False + + self.xls_rowmax = 1048576 + self.xls_colmax = 16384 + self.xls_strmax = 32767 + self.dim_rowmin = None + self.dim_rowmax = None + self.dim_colmin = None + self.dim_colmax = None + + self.col_info = {} + self.selections = [] + self.hidden = 0 + self.active = 0 + self.tab_color = 0 + self.top_left_cell = "" + + self.panes = [] + self.active_pane = 3 + self.selected = 0 + + self.page_setup_changed = False + self.paper_size = 0 + self.orientation = 1 + + self.print_options_changed = False + self.hcenter = False + self.vcenter = False + self.print_gridlines = False + self.screen_gridlines = True + self.print_headers = False + self.row_col_headers = False + + self.header_footer_changed = False + self.header = "" + self.footer = "" + self.header_footer_aligns = True + self.header_footer_scales = True + self.header_images = [] + self.footer_images = [] + self.header_images_list = [] + + self.margin_left = 0.7 + self.margin_right = 0.7 + self.margin_top = 0.75 + self.margin_bottom = 0.75 + self.margin_header = 0.3 + self.margin_footer = 0.3 + + self.repeat_row_range = "" + self.repeat_col_range = "" + self.print_area_range = "" + + self.page_order = 0 + self.black_white = 0 + self.draft_quality = 0 + self.print_comments = 0 + self.page_start = 0 + + self.fit_page = 0 + self.fit_width = 0 + self.fit_height = 0 + + self.hbreaks = [] + self.vbreaks = [] + + self.protect_options = {} + self.protected_ranges = [] + self.num_protected_ranges = 0 + self.set_cols = {} + self.set_rows = defaultdict(dict) + + self.zoom = 100 + self.zoom_scale_normal = 1 + self.print_scale = 100 + self.is_right_to_left = 0 + self.show_zeros = 1 + self.leading_zeros = 0 + + self.outline_row_level = 0 + self.outline_col_level = 0 + self.outline_style = 0 + self.outline_below = 1 + self.outline_right = 1 + self.outline_on = 1 + self.outline_changed = False + + self.original_row_height = 15 + self.default_row_height = 15 + self.default_row_pixels = 20 + self.default_col_width = 8.43 + self.default_col_pixels = 64 + self.default_date_pixels = 68 + self.default_row_zeroed = 0 + + self.names = {} + self.write_match = [] + self.table = defaultdict(dict) + self.merge = [] + self.merged_cells = {} + self.table_cells = {} + self.row_spans = {} + + self.has_vml = False + self.has_header_vml = False + self.has_comments = False + self.comments = defaultdict(dict) + self.comments_list = [] + self.comments_author = "" + self.comments_visible = 0 + self.vml_shape_id = 1024 + self.buttons_list = [] + self.vml_header_id = 0 + + self.autofilter_area = "" + self.autofilter_ref = None + self.filter_range = [0, 9] + self.filter_on = 0 + self.filter_cols = {} + self.filter_type = {} + self.filter_cells = {} + + self.row_sizes = {} + self.col_size_changed = False + self.row_size_changed = False + + self.last_shape_id = 1 + self.rel_count = 0 + self.hlink_count = 0 + self.hlink_refs = [] + self.external_hyper_links = [] + self.external_drawing_links = [] + self.external_comment_links = [] + self.external_vml_links = [] + self.external_table_links = [] + self.external_background_links = [] + self.drawing_links = [] + self.vml_drawing_links = [] + self.charts = [] + self.images = [] + self.tables = [] + self.sparklines = [] + self.shapes = [] + self.shape_hash = {} + self.drawing = 0 + self.drawing_rels = {} + self.drawing_rels_id = 0 + self.vml_drawing_rels = {} + self.vml_drawing_rels_id = 0 + self.background_image = None + self.background_bytes = False + + self.rstring = "" + self.previous_row = 0 + + self.validations = [] + self.cond_formats = {} + self.data_bars_2010 = [] + self.use_data_bars_2010 = False + self.dxf_priority = 1 + self.page_view = 0 + + self.vba_codename = None + + self.date_1904 = False + self.hyperlinks = defaultdict(dict) + + self.strings_to_numbers = False + self.strings_to_urls = True + self.nan_inf_to_errors = False + self.strings_to_formulas = True + + self.default_date_format = None + self.default_url_format = None + self.default_checkbox_format = None + self.workbook_add_format = None + self.remove_timezone = False + self.max_url_length = 2079 + + self.row_data_filename = None + self.row_data_fh = None + self.worksheet_meta = None + self.vml_data_id = None + self.vml_shape_id = None + + self.row_data_filename = None + self.row_data_fh = None + self.row_data_fh_closed = False + + self.vertical_dpi = 0 + self.horizontal_dpi = 0 + + self.write_handlers = {} + + self.ignored_errors = None + + self.has_dynamic_arrays = False + self.use_future_functions = False + self.ignore_write_string = False + self.embedded_images = None + + # Utility function for writing different types of strings. + def _write_token_as_string(self, token, row, col, *args): + # Map the data to the appropriate write_*() method. + if token == "": + return self._write_blank(row, col, *args) + + if self.strings_to_formulas and token.startswith("="): + return self._write_formula(row, col, *args) + + if token.startswith("{=") and token.endswith("}"): + return self._write_formula(row, col, *args) + + if ( + ":" in token + and self.strings_to_urls + and ( + re.match("(ftp|http)s?://", token) + or re.match("mailto:", token) + or re.match("(in|ex)ternal:", token) + ) + ): + return self._write_url(row, col, *args) + + if self.strings_to_numbers: + try: + f = float(token) + if self.nan_inf_to_errors or (not isnan(f) and not isinf(f)): + return self._write_number(row, col, f, *args[1:]) + except ValueError: + # Not a number, write as a string. + pass + + return self._write_string(row, col, *args) + + # We have a plain string. + return self._write_string(row, col, *args) + + @convert_cell_args + def write(self, row, col, *args): + """ + Write data to a worksheet cell by calling the appropriate write_*() + method based on the type of data being passed. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + *args: Args to pass to sub functions. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + other: Return value of called method. + + """ + return self._write(row, col, *args) + + # Undecorated version of write(). + def _write(self, row, col, *args): + # pylint: disable=raise-missing-from + # Check the number of args passed. + if not args: + raise TypeError("write() takes at least 4 arguments (3 given)") + + # The first arg should be the token for all write calls. + token = args[0] + + # Avoid isinstance() for better performance. + token_type = token.__class__ + + # Check for any user defined type handlers with callback functions. + if token_type in self.write_handlers: + write_handler = self.write_handlers[token_type] + function_return = write_handler(self, row, col, *args) + + # If the return value is None then the callback has returned + # control to this function and we should continue as + # normal. Otherwise we return the value to the caller and exit. + if function_return is None: + pass + else: + return function_return + + # Write None as a blank cell. + if token is None: + return self._write_blank(row, col, *args) + + # Check for standard Python types. + if token_type is bool: + return self._write_boolean(row, col, *args) + + if token_type in (float, int, Decimal, Fraction): + return self._write_number(row, col, *args) + + if token_type is str: + return self._write_token_as_string(token, row, col, *args) + + if token_type in ( + datetime.datetime, + datetime.date, + datetime.time, + datetime.timedelta, + ): + return self._write_datetime(row, col, *args) + + # Resort to isinstance() for subclassed primitives. + + # Write number types. + if isinstance(token, (float, int, Decimal, Fraction)): + return self._write_number(row, col, *args) + + # Write string types. + if isinstance(token, str): + return self._write_token_as_string(token, row, col, *args) + + # Write boolean types. + if isinstance(token, bool): + return self._write_boolean(row, col, *args) + + # Write datetime objects. + if _supported_datetime(token): + return self._write_datetime(row, col, *args) + + # We haven't matched a supported type. Try float. + try: + f = float(token) + return self._write_number(row, col, f, *args[1:]) + except ValueError: + pass + except TypeError: + raise TypeError(f"Unsupported type {type(token)} in write()") + + # Finally try string. + try: + str(token) + return self._write_string(row, col, *args) + except ValueError: + raise TypeError(f"Unsupported type {type(token)} in write()") + + @convert_cell_args + def write_string(self, row, col, string, cell_format=None): + """ + Write a string to a worksheet cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + string: Cell data. Str. + format: An optional cell Format object. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + -2: String truncated to 32k characters. + + """ + return self._write_string(row, col, string, cell_format) + + # Undecorated version of write_string(). + def _write_string(self, row, col, string, cell_format=None): + str_error = 0 + + # Check that row and col are valid and store max and min values. + if self._check_dimensions(row, col): + return -1 + + # Check that the string is < 32767 chars. + if len(string) > self.xls_strmax: + string = string[: self.xls_strmax] + str_error = -2 + + # Write a shared string or an in-line string in constant_memory mode. + if not self.constant_memory: + string_index = self.str_table._get_shared_string_index(string) + else: + string_index = string + + # Write previous row if in in-line string constant_memory mode. + if self.constant_memory and row > self.previous_row: + self._write_single_row(row) + + # Store the cell data in the worksheet data table. + self.table[row][col] = CellStringTuple(string_index, cell_format) + + return str_error + + @convert_cell_args + def write_number(self, row, col, number, cell_format=None): + """ + Write a number to a worksheet cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + number: Cell data. Int or float. + cell_format: An optional cell Format object. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + + """ + return self._write_number(row, col, number, cell_format) + + # Undecorated version of write_number(). + def _write_number(self, row, col, number, cell_format=None): + if isnan(number) or isinf(number): + if self.nan_inf_to_errors: + if isnan(number): + return self._write_formula(row, col, "#NUM!", cell_format, "#NUM!") + + if number == math.inf: + return self._write_formula(row, col, "1/0", cell_format, "#DIV/0!") + + if number == -math.inf: + return self._write_formula(row, col, "-1/0", cell_format, "#DIV/0!") + else: + raise TypeError( + "NAN/INF not supported in write_number() " + "without 'nan_inf_to_errors' Workbook() option" + ) + + if number.__class__ is Fraction: + number = float(number) + + # Check that row and col are valid and store max and min values. + if self._check_dimensions(row, col): + return -1 + + # Write previous row if in in-line string constant_memory mode. + if self.constant_memory and row > self.previous_row: + self._write_single_row(row) + + # Store the cell data in the worksheet data table. + self.table[row][col] = CellNumberTuple(number, cell_format) + + return 0 + + @convert_cell_args + def write_blank(self, row, col, blank, cell_format=None): + """ + Write a blank cell with formatting to a worksheet cell. The blank + token is ignored and the format only is written to the cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + blank: Any value. It is ignored. + cell_format: An optional cell Format object. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + + """ + return self._write_blank(row, col, blank, cell_format) + + # Undecorated version of write_blank(). + def _write_blank(self, row, col, _, cell_format=None): + # Don't write a blank cell unless it has a format. + if cell_format is None: + return 0 + + # Check that row and col are valid and store max and min values. + if self._check_dimensions(row, col): + return -1 + + # Write previous row if in in-line string constant_memory mode. + if self.constant_memory and row > self.previous_row: + self._write_single_row(row) + + # Store the cell data in the worksheet data table. + self.table[row][col] = CellBlankTuple(cell_format) + + return 0 + + @convert_cell_args + def write_formula(self, row, col, formula, cell_format=None, value=0): + """ + Write a formula to a worksheet cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + formula: Cell formula. + cell_format: An optional cell Format object. + value: An optional value for the formula. Default is 0. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + -2: Formula can't be None or empty. + + """ + # Check that row and col are valid and store max and min values. + return self._write_formula(row, col, formula, cell_format, value) + + # Undecorated version of write_formula(). + def _write_formula(self, row, col, formula, cell_format=None, value=0): + if self._check_dimensions(row, col): + return -1 + + if formula is None or formula == "": + warn("Formula can't be None or empty") + return -1 + + # Check for dynamic array functions. + if re_dynamic_function.search(formula): + return self.write_dynamic_array_formula( + row, col, row, col, formula, cell_format, value + ) + + # Hand off array formulas. + if formula.startswith("{") and formula.endswith("}"): + return self._write_array_formula( + row, col, row, col, formula, cell_format, value + ) + + # Modify the formula string, as needed. + formula = self._prepare_formula(formula) + + # Write previous row if in in-line string constant_memory mode. + if self.constant_memory and row > self.previous_row: + self._write_single_row(row) + + # Store the cell data in the worksheet data table. + self.table[row][col] = CellFormulaTuple(formula, cell_format, value) + + return 0 + + @convert_range_args + def write_array_formula( + self, + first_row, + first_col, + last_row, + last_col, + formula, + cell_format=None, + value=0, + ): + """ + Write a formula to a worksheet cell/range. + + Args: + first_row: The first row of the cell range. (zero indexed). + first_col: The first column of the cell range. + last_row: The last row of the cell range. (zero indexed). + last_col: The last column of the cell range. + formula: Cell formula. + cell_format: An optional cell Format object. + value: An optional value for the formula. Default is 0. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + + """ + # Check for dynamic array functions. + if re_dynamic_function.search(formula): + return self.write_dynamic_array_formula( + first_row, first_col, last_row, last_col, formula, cell_format, value + ) + + return self._write_array_formula( + first_row, + first_col, + last_row, + last_col, + formula, + cell_format, + value, + "static", + ) + + @convert_range_args + def write_dynamic_array_formula( + self, + first_row, + first_col, + last_row, + last_col, + formula, + cell_format=None, + value=0, + ): + """ + Write a dynamic array formula to a worksheet cell/range. + + Args: + first_row: The first row of the cell range. (zero indexed). + first_col: The first column of the cell range. + last_row: The last row of the cell range. (zero indexed). + last_col: The last column of the cell range. + formula: Cell formula. + cell_format: An optional cell Format object. + value: An optional value for the formula. Default is 0. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + + """ + error = self._write_array_formula( + first_row, + first_col, + last_row, + last_col, + formula, + cell_format, + value, + "dynamic", + ) + + if error == 0: + self.has_dynamic_arrays = True + + return error + + # Utility method to strip equal sign and array braces from a formula and + # also expand out future and dynamic array formulas. + def _prepare_formula(self, formula, expand_future_functions=False): + # Remove array formula braces and the leading =. + if formula.startswith("{"): + formula = formula[1:] + if formula.startswith("="): + formula = formula[1:] + if formula.endswith("}"): + formula = formula[:-1] + + # Check if formula is already expanded by the user. + if "_xlfn." in formula: + return formula + + # Expand dynamic formulas. + formula = re.sub(r"\bANCHORARRAY\(", "_xlfn.ANCHORARRAY(", formula) + formula = re.sub(r"\bBYCOL\(", "_xlfn.BYCOL(", formula) + formula = re.sub(r"\bBYROW\(", "_xlfn.BYROW(", formula) + formula = re.sub(r"\bCHOOSECOLS\(", "_xlfn.CHOOSECOLS(", formula) + formula = re.sub(r"\bCHOOSEROWS\(", "_xlfn.CHOOSEROWS(", formula) + formula = re.sub(r"\bDROP\(", "_xlfn.DROP(", formula) + formula = re.sub(r"\bEXPAND\(", "_xlfn.EXPAND(", formula) + formula = re.sub(r"\bFILTER\(", "_xlfn._xlws.FILTER(", formula) + formula = re.sub(r"\bHSTACK\(", "_xlfn.HSTACK(", formula) + formula = re.sub(r"\bLAMBDA\(", "_xlfn.LAMBDA(", formula) + formula = re.sub(r"\bMAKEARRAY\(", "_xlfn.MAKEARRAY(", formula) + formula = re.sub(r"\bMAP\(", "_xlfn.MAP(", formula) + formula = re.sub(r"\bRANDARRAY\(", "_xlfn.RANDARRAY(", formula) + formula = re.sub(r"\bREDUCE\(", "_xlfn.REDUCE(", formula) + formula = re.sub(r"\bSCAN\(", "_xlfn.SCAN(", formula) + formula = re.sub(r"\SINGLE\(", "_xlfn.SINGLE(", formula) + formula = re.sub(r"\bSEQUENCE\(", "_xlfn.SEQUENCE(", formula) + formula = re.sub(r"\bSORT\(", "_xlfn._xlws.SORT(", formula) + formula = re.sub(r"\bSORTBY\(", "_xlfn.SORTBY(", formula) + formula = re.sub(r"\bSWITCH\(", "_xlfn.SWITCH(", formula) + formula = re.sub(r"\bTAKE\(", "_xlfn.TAKE(", formula) + formula = re.sub(r"\bTEXTSPLIT\(", "_xlfn.TEXTSPLIT(", formula) + formula = re.sub(r"\bTOCOL\(", "_xlfn.TOCOL(", formula) + formula = re.sub(r"\bTOROW\(", "_xlfn.TOROW(", formula) + formula = re.sub(r"\bUNIQUE\(", "_xlfn.UNIQUE(", formula) + formula = re.sub(r"\bVSTACK\(", "_xlfn.VSTACK(", formula) + formula = re.sub(r"\bWRAPCOLS\(", "_xlfn.WRAPCOLS(", formula) + formula = re.sub(r"\bWRAPROWS\(", "_xlfn.WRAPROWS(", formula) + formula = re.sub(r"\bXLOOKUP\(", "_xlfn.XLOOKUP(", formula) + + if not self.use_future_functions and not expand_future_functions: + return formula + + formula = re.sub(r"\bACOTH\(", "_xlfn.ACOTH(", formula) + formula = re.sub(r"\bACOT\(", "_xlfn.ACOT(", formula) + formula = re.sub(r"\bAGGREGATE\(", "_xlfn.AGGREGATE(", formula) + formula = re.sub(r"\bARABIC\(", "_xlfn.ARABIC(", formula) + formula = re.sub(r"\bARRAYTOTEXT\(", "_xlfn.ARRAYTOTEXT(", formula) + formula = re.sub(r"\bBASE\(", "_xlfn.BASE(", formula) + formula = re.sub(r"\bBETA.DIST\(", "_xlfn.BETA.DIST(", formula) + formula = re.sub(r"\bBETA.INV\(", "_xlfn.BETA.INV(", formula) + formula = re.sub(r"\bBINOM.DIST.RANGE\(", "_xlfn.BINOM.DIST.RANGE(", formula) + formula = re.sub(r"\bBINOM.DIST\(", "_xlfn.BINOM.DIST(", formula) + formula = re.sub(r"\bBINOM.INV\(", "_xlfn.BINOM.INV(", formula) + formula = re.sub(r"\bBITAND\(", "_xlfn.BITAND(", formula) + formula = re.sub(r"\bBITLSHIFT\(", "_xlfn.BITLSHIFT(", formula) + formula = re.sub(r"\bBITOR\(", "_xlfn.BITOR(", formula) + formula = re.sub(r"\bBITRSHIFT\(", "_xlfn.BITRSHIFT(", formula) + formula = re.sub(r"\bBITXOR\(", "_xlfn.BITXOR(", formula) + formula = re.sub(r"\bCEILING.MATH\(", "_xlfn.CEILING.MATH(", formula) + formula = re.sub(r"\bCEILING.PRECISE\(", "_xlfn.CEILING.PRECISE(", formula) + formula = re.sub(r"\bCHISQ.DIST.RT\(", "_xlfn.CHISQ.DIST.RT(", formula) + formula = re.sub(r"\bCHISQ.DIST\(", "_xlfn.CHISQ.DIST(", formula) + formula = re.sub(r"\bCHISQ.INV.RT\(", "_xlfn.CHISQ.INV.RT(", formula) + formula = re.sub(r"\bCHISQ.INV\(", "_xlfn.CHISQ.INV(", formula) + formula = re.sub(r"\bCHISQ.TEST\(", "_xlfn.CHISQ.TEST(", formula) + formula = re.sub(r"\bCOMBINA\(", "_xlfn.COMBINA(", formula) + formula = re.sub(r"\bCONCAT\(", "_xlfn.CONCAT(", formula) + formula = re.sub(r"\bCONFIDENCE.NORM\(", "_xlfn.CONFIDENCE.NORM(", formula) + formula = re.sub(r"\bCONFIDENCE.T\(", "_xlfn.CONFIDENCE.T(", formula) + formula = re.sub(r"\bCOTH\(", "_xlfn.COTH(", formula) + formula = re.sub(r"\bCOT\(", "_xlfn.COT(", formula) + formula = re.sub(r"\bCOVARIANCE.P\(", "_xlfn.COVARIANCE.P(", formula) + formula = re.sub(r"\bCOVARIANCE.S\(", "_xlfn.COVARIANCE.S(", formula) + formula = re.sub(r"\bCSCH\(", "_xlfn.CSCH(", formula) + formula = re.sub(r"\bCSC\(", "_xlfn.CSC(", formula) + formula = re.sub(r"\bDAYS\(", "_xlfn.DAYS(", formula) + formula = re.sub(r"\bDECIMAL\(", "_xlfn.DECIMAL(", formula) + formula = re.sub(r"\bERF.PRECISE\(", "_xlfn.ERF.PRECISE(", formula) + formula = re.sub(r"\bERFC.PRECISE\(", "_xlfn.ERFC.PRECISE(", formula) + formula = re.sub(r"\bEXPON.DIST\(", "_xlfn.EXPON.DIST(", formula) + formula = re.sub(r"\bF.DIST.RT\(", "_xlfn.F.DIST.RT(", formula) + formula = re.sub(r"\bF.DIST\(", "_xlfn.F.DIST(", formula) + formula = re.sub(r"\bF.INV.RT\(", "_xlfn.F.INV.RT(", formula) + formula = re.sub(r"\bF.INV\(", "_xlfn.F.INV(", formula) + formula = re.sub(r"\bF.TEST\(", "_xlfn.F.TEST(", formula) + formula = re.sub(r"\bFILTERXML\(", "_xlfn.FILTERXML(", formula) + formula = re.sub(r"\bFLOOR.MATH\(", "_xlfn.FLOOR.MATH(", formula) + formula = re.sub(r"\bFLOOR.PRECISE\(", "_xlfn.FLOOR.PRECISE(", formula) + formula = re.sub( + r"\bFORECAST.ETS.CONFINT\(", "_xlfn.FORECAST.ETS.CONFINT(", formula + ) + formula = re.sub( + r"\bFORECAST.ETS.SEASONALITY\(", "_xlfn.FORECAST.ETS.SEASONALITY(", formula + ) + formula = re.sub(r"\bFORECAST.ETS.STAT\(", "_xlfn.FORECAST.ETS.STAT(", formula) + formula = re.sub(r"\bFORECAST.ETS\(", "_xlfn.FORECAST.ETS(", formula) + formula = re.sub(r"\bFORECAST.LINEAR\(", "_xlfn.FORECAST.LINEAR(", formula) + formula = re.sub(r"\bFORMULATEXT\(", "_xlfn.FORMULATEXT(", formula) + formula = re.sub(r"\bGAMMA.DIST\(", "_xlfn.GAMMA.DIST(", formula) + formula = re.sub(r"\bGAMMA.INV\(", "_xlfn.GAMMA.INV(", formula) + formula = re.sub(r"\bGAMMALN.PRECISE\(", "_xlfn.GAMMALN.PRECISE(", formula) + formula = re.sub(r"\bGAMMA\(", "_xlfn.GAMMA(", formula) + formula = re.sub(r"\bGAUSS\(", "_xlfn.GAUSS(", formula) + formula = re.sub(r"\bHYPGEOM.DIST\(", "_xlfn.HYPGEOM.DIST(", formula) + formula = re.sub(r"\bIFNA\(", "_xlfn.IFNA(", formula) + formula = re.sub(r"\bIFS\(", "_xlfn.IFS(", formula) + formula = re.sub(r"\bIMAGE\(", "_xlfn.IMAGE(", formula) + formula = re.sub(r"\bIMCOSH\(", "_xlfn.IMCOSH(", formula) + formula = re.sub(r"\bIMCOT\(", "_xlfn.IMCOT(", formula) + formula = re.sub(r"\bIMCSCH\(", "_xlfn.IMCSCH(", formula) + formula = re.sub(r"\bIMCSC\(", "_xlfn.IMCSC(", formula) + formula = re.sub(r"\bIMSECH\(", "_xlfn.IMSECH(", formula) + formula = re.sub(r"\bIMSEC\(", "_xlfn.IMSEC(", formula) + formula = re.sub(r"\bIMSINH\(", "_xlfn.IMSINH(", formula) + formula = re.sub(r"\bIMTAN\(", "_xlfn.IMTAN(", formula) + formula = re.sub(r"\bISFORMULA\(", "_xlfn.ISFORMULA(", formula) + formula = re.sub(r"\bISOMITTED\(", "_xlfn.ISOMITTED(", formula) + formula = re.sub(r"\bISOWEEKNUM\(", "_xlfn.ISOWEEKNUM(", formula) + formula = re.sub(r"\bLET\(", "_xlfn.LET(", formula) + formula = re.sub(r"\bLOGNORM.DIST\(", "_xlfn.LOGNORM.DIST(", formula) + formula = re.sub(r"\bLOGNORM.INV\(", "_xlfn.LOGNORM.INV(", formula) + formula = re.sub(r"\bMAXIFS\(", "_xlfn.MAXIFS(", formula) + formula = re.sub(r"\bMINIFS\(", "_xlfn.MINIFS(", formula) + formula = re.sub(r"\bMODE.MULT\(", "_xlfn.MODE.MULT(", formula) + formula = re.sub(r"\bMODE.SNGL\(", "_xlfn.MODE.SNGL(", formula) + formula = re.sub(r"\bMUNIT\(", "_xlfn.MUNIT(", formula) + formula = re.sub(r"\bNEGBINOM.DIST\(", "_xlfn.NEGBINOM.DIST(", formula) + formula = re.sub(r"\bNORM.DIST\(", "_xlfn.NORM.DIST(", formula) + formula = re.sub(r"\bNORM.INV\(", "_xlfn.NORM.INV(", formula) + formula = re.sub(r"\bNORM.S.DIST\(", "_xlfn.NORM.S.DIST(", formula) + formula = re.sub(r"\bNORM.S.INV\(", "_xlfn.NORM.S.INV(", formula) + formula = re.sub(r"\bNUMBERVALUE\(", "_xlfn.NUMBERVALUE(", formula) + formula = re.sub(r"\bPDURATION\(", "_xlfn.PDURATION(", formula) + formula = re.sub(r"\bPERCENTILE.EXC\(", "_xlfn.PERCENTILE.EXC(", formula) + formula = re.sub(r"\bPERCENTILE.INC\(", "_xlfn.PERCENTILE.INC(", formula) + formula = re.sub(r"\bPERCENTRANK.EXC\(", "_xlfn.PERCENTRANK.EXC(", formula) + formula = re.sub(r"\bPERCENTRANK.INC\(", "_xlfn.PERCENTRANK.INC(", formula) + formula = re.sub(r"\bPERMUTATIONA\(", "_xlfn.PERMUTATIONA(", formula) + formula = re.sub(r"\bPHI\(", "_xlfn.PHI(", formula) + formula = re.sub(r"\bPOISSON.DIST\(", "_xlfn.POISSON.DIST(", formula) + formula = re.sub(r"\bQUARTILE.EXC\(", "_xlfn.QUARTILE.EXC(", formula) + formula = re.sub(r"\bQUARTILE.INC\(", "_xlfn.QUARTILE.INC(", formula) + formula = re.sub(r"\bQUERYSTRING\(", "_xlfn.QUERYSTRING(", formula) + formula = re.sub(r"\bRANK.AVG\(", "_xlfn.RANK.AVG(", formula) + formula = re.sub(r"\bRANK.EQ\(", "_xlfn.RANK.EQ(", formula) + formula = re.sub(r"\bRRI\(", "_xlfn.RRI(", formula) + formula = re.sub(r"\bSECH\(", "_xlfn.SECH(", formula) + formula = re.sub(r"\bSEC\(", "_xlfn.SEC(", formula) + formula = re.sub(r"\bSHEETS\(", "_xlfn.SHEETS(", formula) + formula = re.sub(r"\bSHEET\(", "_xlfn.SHEET(", formula) + formula = re.sub(r"\bSKEW.P\(", "_xlfn.SKEW.P(", formula) + formula = re.sub(r"\bSTDEV.P\(", "_xlfn.STDEV.P(", formula) + formula = re.sub(r"\bSTDEV.S\(", "_xlfn.STDEV.S(", formula) + formula = re.sub(r"\bT.DIST.2T\(", "_xlfn.T.DIST.2T(", formula) + formula = re.sub(r"\bT.DIST.RT\(", "_xlfn.T.DIST.RT(", formula) + formula = re.sub(r"\bT.DIST\(", "_xlfn.T.DIST(", formula) + formula = re.sub(r"\bT.INV.2T\(", "_xlfn.T.INV.2T(", formula) + formula = re.sub(r"\bT.INV\(", "_xlfn.T.INV(", formula) + formula = re.sub(r"\bT.TEST\(", "_xlfn.T.TEST(", formula) + formula = re.sub(r"\bTEXTAFTER\(", "_xlfn.TEXTAFTER(", formula) + formula = re.sub(r"\bTEXTBEFORE\(", "_xlfn.TEXTBEFORE(", formula) + formula = re.sub(r"\bTEXTJOIN\(", "_xlfn.TEXTJOIN(", formula) + formula = re.sub(r"\bUNICHAR\(", "_xlfn.UNICHAR(", formula) + formula = re.sub(r"\bUNICODE\(", "_xlfn.UNICODE(", formula) + formula = re.sub(r"\bVALUETOTEXT\(", "_xlfn.VALUETOTEXT(", formula) + formula = re.sub(r"\bVAR.P\(", "_xlfn.VAR.P(", formula) + formula = re.sub(r"\bVAR.S\(", "_xlfn.VAR.S(", formula) + formula = re.sub(r"\bWEBSERVICE\(", "_xlfn.WEBSERVICE(", formula) + formula = re.sub(r"\bWEIBULL.DIST\(", "_xlfn.WEIBULL.DIST(", formula) + formula = re.sub(r"\bXMATCH\(", "_xlfn.XMATCH(", formula) + formula = re.sub(r"\bXOR\(", "_xlfn.XOR(", formula) + formula = re.sub(r"\bZ.TEST\(", "_xlfn.Z.TEST(", formula) + + return formula + + # Escape/expand table functions. This mainly involves converting Excel 2010 + # "@" table ref to 2007 "[#This Row],". We parse the string to avoid + # replacements in string literals within the formula. + @staticmethod + def _prepare_table_formula(formula): + if "@" not in formula: + # No escaping required. + return formula + + escaped_formula = [] + in_string_literal = False + + for char in formula: + # Match the start/end of string literals to avoid escaping + # references in strings. + if char == '"': + in_string_literal = not in_string_literal + + # Copy the string literal. + if in_string_literal: + escaped_formula.append(char) + continue + + # Replace table reference. + if char == "@": + escaped_formula.append("[#This Row],") + else: + escaped_formula.append(char) + + return ("").join(escaped_formula) + + # Undecorated version of write_array_formula() and + # write_dynamic_array_formula(). + def _write_array_formula( + self, + first_row, + first_col, + last_row, + last_col, + formula, + cell_format=None, + value=0, + atype="static", + ): + # Swap last row/col with first row/col as necessary. + if first_row > last_row: + first_row, last_row = last_row, first_row + if first_col > last_col: + first_col, last_col = last_col, first_col + + # Check that row and col are valid and store max and min values. + if self._check_dimensions(first_row, first_col): + return -1 + if self._check_dimensions(last_row, last_col): + return -1 + + # Define array range + if first_row == last_row and first_col == last_col: + cell_range = xl_rowcol_to_cell(first_row, first_col) + else: + cell_range = ( + xl_rowcol_to_cell(first_row, first_col) + + ":" + + xl_rowcol_to_cell(last_row, last_col) + ) + + # Modify the formula string, as needed. + formula = self._prepare_formula(formula) + + # Write previous row if in in-line string constant_memory mode. + if self.constant_memory and first_row > self.previous_row: + self._write_single_row(first_row) + + # Store the cell data in the worksheet data table. + self.table[first_row][first_col] = CellArrayFormulaTuple( + formula, cell_format, value, cell_range, atype + ) + + # Pad out the rest of the area with formatted zeroes. + if not self.constant_memory: + for row in range(first_row, last_row + 1): + for col in range(first_col, last_col + 1): + if row != first_row or col != first_col: + self._write_number(row, col, 0, cell_format) + + return 0 + + @convert_cell_args + def write_datetime(self, row, col, date, cell_format=None): + """ + Write a date or time to a worksheet cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + date: Date and/or time as a datetime object. + cell_format: A cell Format object. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + + """ + return self._write_datetime(row, col, date, cell_format) + + # Undecorated version of write_datetime(). + def _write_datetime(self, row, col, date, cell_format=None): + # Check that row and col are valid and store max and min values. + if self._check_dimensions(row, col): + return -1 + + # Write previous row if in in-line string constant_memory mode. + if self.constant_memory and row > self.previous_row: + self._write_single_row(row) + + # Convert datetime to an Excel date. + number = self._convert_date_time(date) + + # Add the default date format. + if cell_format is None: + cell_format = self.default_date_format + + # Store the cell data in the worksheet data table. + self.table[row][col] = CellDatetimeTuple(number, cell_format) + + return 0 + + @convert_cell_args + def write_boolean(self, row, col, boolean, cell_format=None): + """ + Write a boolean value to a worksheet cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + boolean: Cell data. bool type. + cell_format: An optional cell Format object. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + + """ + return self._write_boolean(row, col, boolean, cell_format) + + # Undecorated version of write_boolean(). + def _write_boolean(self, row, col, boolean, cell_format=None): + # Check that row and col are valid and store max and min values. + if self._check_dimensions(row, col): + return -1 + + # Write previous row if in in-line string constant_memory mode. + if self.constant_memory and row > self.previous_row: + self._write_single_row(row) + + if boolean: + value = 1 + else: + value = 0 + + # Store the cell data in the worksheet data table. + self.table[row][col] = CellBooleanTuple(value, cell_format) + + return 0 + + # Write a hyperlink. This is comprised of two elements: the displayed + # string and the non-displayed link. The displayed string is the same as + # the link unless an alternative string is specified. The display string + # is written using the write_string() method. Therefore the max characters + # string limit applies. + # + # The hyperlink can be to a http, ftp, mail, internal sheet, or external + # directory urls. + @convert_cell_args + def write_url(self, row, col, url, cell_format=None, string=None, tip=None): + """ + Write a hyperlink to a worksheet cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + url: Hyperlink url. + format: An optional cell Format object. + string: An optional display string for the hyperlink. + tip: An optional tooltip. + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + -2: String longer than 32767 characters. + -3: URL longer than Excel limit of 255 characters. + -4: Exceeds Excel limit of 65,530 urls per worksheet. + """ + return self._write_url(row, col, url, cell_format, string, tip) + + # Undecorated version of write_url(). + def _write_url(self, row, col, url, cell_format=None, string=None, tip=None): + # Check that row and col are valid and store max and min values + if self._check_dimensions(row, col): + return -1 + + # Set the displayed string to the URL unless defined by the user. + if string is None: + string = url + + # Default to external link type such as 'http://' or 'external:'. + link_type = 1 + + # Remove the URI scheme from internal links. + if url.startswith("internal:"): + url = url.replace("internal:", "") + string = string.replace("internal:", "") + link_type = 2 + + # Remove the URI scheme from external links and change the directory + # separator from Unix to Dos. + external = False + if url.startswith("external:"): + url = url.replace("external:", "") + url = url.replace("/", "\\") + string = string.replace("external:", "") + string = string.replace("/", "\\") + external = True + + # Strip the mailto header. + string = string.replace("mailto:", "") + + # Check that the string is < 32767 chars + str_error = 0 + if len(string) > self.xls_strmax: + warn( + "Ignoring URL since it exceeds Excel's string limit of " + "32767 characters" + ) + return -2 + + # Copy string for use in hyperlink elements. + url_str = string + + # External links to URLs and to other Excel workbooks have slightly + # different characteristics that we have to account for. + if link_type == 1: + # Split url into the link and optional anchor/location. + if "#" in url: + url, url_str = url.split("#", 1) + else: + url_str = None + + url = self._escape_url(url) + + if url_str is not None and not external: + url_str = self._escape_url(url_str) + + # Add the file:/// URI to the url for Windows style "C:/" link and + # Network shares. + if re.match(r"\w:", url) or re.match(r"\\", url): + url = "file:///" + url + + # Convert a .\dir\file.xlsx link to dir\file.xlsx. + url = re.sub(r"^\.\\", "", url) + + # Excel limits the escaped URL and location/anchor to 255 characters. + tmp_url_str = url_str or "" + max_url = self.max_url_length + if len(url) > max_url or len(tmp_url_str) > max_url: + warn( + f"Ignoring URL '{url}' with link or location/anchor > {max_url} " + f"characters since it exceeds Excel's limit for URLs." + ) + return -3 + + # Check the limit of URLs per worksheet. + self.hlink_count += 1 + + if self.hlink_count > 65530: + warn( + f"Ignoring URL '{url}' since it exceeds Excel's limit of " + f"65,530 URLs per worksheet." + ) + return -4 + + # Add the default URL format. + if cell_format is None: + cell_format = self.default_url_format + + if not self.ignore_write_string: + # Write previous row if in in-line string constant_memory mode. + if self.constant_memory and row > self.previous_row: + self._write_single_row(row) + + # Write the hyperlink string. + self._write_string(row, col, string, cell_format) + + # Store the hyperlink data in a separate structure. + self.hyperlinks[row][col] = { + "link_type": link_type, + "url": url, + "str": url_str, + "tip": tip, + } + + return str_error + + @convert_cell_args + def write_rich_string(self, row, col, *args): + """ + Write a "rich" string with multiple formats to a worksheet cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + string_parts: String and format pairs. + cell_format: Optional Format object. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + -2: String truncated to 32k characters. + -3: 2 consecutive formats used. + -4: Empty string used. + -5: Insufficient parameters. + + """ + + return self._write_rich_string(row, col, *args) + + # Undecorated version of write_rich_string(). + def _write_rich_string(self, row, col, *args): + tokens = list(args) + cell_format = None + string_index = 0 + raw_string = "" + + # Check that row and col are valid and store max and min values + if self._check_dimensions(row, col): + return -1 + + # If the last arg is a format we use it as the cell format. + if isinstance(tokens[-1], Format): + cell_format = tokens.pop() + + # Create a temp XMLWriter object and use it to write the rich string + # XML to a string. + fh = StringIO() + self.rstring = XMLwriter() + self.rstring._set_filehandle(fh) + + # Create a temp format with the default font for unformatted fragments. + default = Format() + + # Convert list of format, string tokens to pairs of (format, string) + # except for the first string fragment which doesn't require a default + # formatting run. Use the default for strings without a leading format. + fragments = [] + previous = "format" + pos = 0 + + if len(tokens) <= 2: + warn( + "You must specify more than 2 format/fragments for rich " + "strings. Ignoring input in write_rich_string()." + ) + return -5 + + for token in tokens: + if not isinstance(token, Format): + # Token is a string. + if previous != "format": + # If previous token wasn't a format add one before string. + fragments.append(default) + fragments.append(token) + else: + # If previous token was a format just add the string. + fragments.append(token) + + if token == "": + warn( + "Excel doesn't allow empty strings in rich strings. " + "Ignoring input in write_rich_string()." + ) + return -4 + + # Keep track of unformatted string. + raw_string += token + previous = "string" + else: + # Can't allow 2 formats in a row. + if previous == "format" and pos > 0: + warn( + "Excel doesn't allow 2 consecutive formats in rich " + "strings. Ignoring input in write_rich_string()." + ) + return -3 + + # Token is a format object. Add it to the fragment list. + fragments.append(token) + previous = "format" + + pos += 1 + + # If the first token is a string start the <r> element. + if not isinstance(fragments[0], Format): + self.rstring._xml_start_tag("r") + + # Write the XML elements for the $format $string fragments. + for token in fragments: + if isinstance(token, Format): + # Write the font run. + self.rstring._xml_start_tag("r") + self._write_font(token) + else: + # Write the string fragment part, with whitespace handling. + attributes = [] + + if _preserve_whitespace(token): + attributes.append(("xml:space", "preserve")) + + self.rstring._xml_data_element("t", token, attributes) + self.rstring._xml_end_tag("r") + + # Read the in-memory string. + string = self.rstring.fh.getvalue() + + # Check that the string is < 32767 chars. + if len(raw_string) > self.xls_strmax: + warn( + "String length must be less than or equal to Excel's limit " + "of 32,767 characters in write_rich_string()." + ) + return -2 + + # Write a shared string or an in-line string in constant_memory mode. + if not self.constant_memory: + string_index = self.str_table._get_shared_string_index(string) + else: + string_index = string + + # Write previous row if in in-line string constant_memory mode. + if self.constant_memory and row > self.previous_row: + self._write_single_row(row) + + # Store the cell data in the worksheet data table. + self.table[row][col] = CellRichStringTuple( + string_index, cell_format, raw_string + ) + + return 0 + + def add_write_handler(self, user_type, user_function): + """ + Add a callback function to the write() method to handle user defined + types. + + Args: + user_type: The user type() to match on. + user_function: The user defined function to write the type data. + Returns: + Nothing. + + """ + + self.write_handlers[user_type] = user_function + + @convert_cell_args + def write_row(self, row, col, data, cell_format=None): + """ + Write a row of data starting from (row, col). + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + data: A list of tokens to be written with write(). + format: An optional cell Format object. + Returns: + 0: Success. + other: Return value of write() method. + + """ + for token in data: + error = self._write(row, col, token, cell_format) + if error: + return error + col += 1 + + return 0 + + @convert_cell_args + def write_column(self, row, col, data, cell_format=None): + """ + Write a column of data starting from (row, col). + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + data: A list of tokens to be written with write(). + format: An optional cell Format object. + Returns: + 0: Success. + other: Return value of write() method. + + """ + for token in data: + error = self._write(row, col, token, cell_format) + if error: + return error + row += 1 + + return 0 + + @convert_cell_args + def insert_image(self, row, col, filename, options=None): + """ + Insert an image with its top-left corner in a worksheet cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + filename: Path and filename for in supported formats. + options: Position, scale, url and data stream of the image. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + + """ + # Check insert (row, col) without storing. + if self._check_dimensions(row, col, True, True): + warn(f"Cannot insert image at ({row}, {col}).") + return -1 + + if options is None: + options = {} + + x_offset = options.get("x_offset", 0) + y_offset = options.get("y_offset", 0) + x_scale = options.get("x_scale", 1) + y_scale = options.get("y_scale", 1) + url = options.get("url", None) + tip = options.get("tip", None) + anchor = options.get("object_position", 2) + image_data = options.get("image_data", None) + description = options.get("description", None) + decorative = options.get("decorative", False) + + # For backward compatibility with older parameter name. + anchor = options.get("positioning", anchor) + + if not image_data and not os.path.exists(filename): + warn(f"Image file '{filename}' not found.") + return -1 + + self.images.append( + [ + row, + col, + filename, + x_offset, + y_offset, + x_scale, + y_scale, + url, + tip, + anchor, + image_data, + description, + decorative, + ] + ) + return 0 + + @convert_cell_args + def embed_image(self, row, col, filename, options=None): + """ + Embed an image in a worksheet cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + filename: Path and filename for in supported formats. + options: Url and data stream of the image. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + + """ + # Check insert (row, col) without storing. + if self._check_dimensions(row, col): + warn(f"Cannot embed image at ({row}, {col}).") + return -1 + + if options is None: + options = {} + + url = options.get("url", None) + tip = options.get("tip", None) + cell_format = options.get("cell_format", None) + image_data = options.get("image_data", None) + description = options.get("description", None) + decorative = options.get("decorative", False) + + if not image_data and not os.path.exists(filename): + warn(f"Image file '{filename}' not found.") + return -1 + + if url: + if cell_format is None: + cell_format = self.default_url_format + + self.ignore_write_string = True + self.write_url(row, col, url, cell_format, None, tip) + self.ignore_write_string = False + + # Get the image properties, for the type and checksum. + ( + image_type, + _, + _, + _, + _, + _, + digest, + ) = _get_image_properties(filename, image_data) + + image = [filename, image_type, image_data, description, decorative] + image_index = self.embedded_images.get_image_index(image, digest) + + # Store the cell error and image index in the worksheet data table. + self.table[row][col] = CellErrorTuple("#VALUE!", cell_format, image_index) + + return 0 + + @convert_cell_args + def insert_textbox(self, row, col, text, options=None): + """ + Insert an textbox with its top-left corner in a worksheet cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + text: The text for the textbox. + options: Textbox options. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + + """ + # Check insert (row, col) without storing. + if self._check_dimensions(row, col, True, True): + warn(f"Cannot insert textbox at ({row}, {col}).") + return -1 + + if text is None: + text = "" + + if options is None: + options = {} + + x_offset = options.get("x_offset", 0) + y_offset = options.get("y_offset", 0) + x_scale = options.get("x_scale", 1) + y_scale = options.get("y_scale", 1) + anchor = options.get("object_position", 1) + description = options.get("description", None) + decorative = options.get("decorative", False) + + self.shapes.append( + [ + row, + col, + x_offset, + y_offset, + x_scale, + y_scale, + text, + anchor, + options, + description, + decorative, + ] + ) + return 0 + + @convert_cell_args + def insert_chart(self, row, col, chart, options=None): + """ + Insert an chart with its top-left corner in a worksheet cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + chart: Chart object. + options: Position and scale of the chart. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + + """ + # Check insert (row, col) without storing. + if self._check_dimensions(row, col, True, True): + warn(f"Cannot insert chart at ({row}, {col}).") + return -1 + + if options is None: + options = {} + + # Ensure a chart isn't inserted more than once. + if chart.already_inserted or chart.combined and chart.combined.already_inserted: + warn("Chart cannot be inserted in a worksheet more than once.") + return -2 + + chart.already_inserted = True + + if chart.combined: + chart.combined.already_inserted = True + + x_offset = options.get("x_offset", 0) + y_offset = options.get("y_offset", 0) + x_scale = options.get("x_scale", 1) + y_scale = options.get("y_scale", 1) + anchor = options.get("object_position", 1) + description = options.get("description", None) + decorative = options.get("decorative", False) + + # Allow Chart to override the scale and offset. + if chart.x_scale != 1: + x_scale = chart.x_scale + + if chart.y_scale != 1: + y_scale = chart.y_scale + + if chart.x_offset: + x_offset = chart.x_offset + + if chart.y_offset: + y_offset = chart.y_offset + + self.charts.append( + [ + row, + col, + chart, + x_offset, + y_offset, + x_scale, + y_scale, + anchor, + description, + decorative, + ] + ) + return 0 + + @convert_cell_args + def write_comment(self, row, col, comment, options=None): + """ + Write a comment to a worksheet cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + comment: Cell comment. Str. + options: Comment formatting options. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + -2: String longer than 32k characters. + + """ + if options is None: + options = {} + + # Check that row and col are valid and store max and min values + if self._check_dimensions(row, col): + return -1 + + # Check that the comment string is < 32767 chars. + if len(comment) > self.xls_strmax: + return -2 + + self.has_vml = 1 + self.has_comments = 1 + + # Store the options of the cell comment, to process on file close. + self.comments[row][col] = [row, col, comment, options] + + return 0 + + def show_comments(self): + """ + Make any comments in the worksheet visible. + + Args: + None. + + Returns: + Nothing. + + """ + self.comments_visible = 1 + + def set_background(self, filename, is_byte_stream=False): + """ + Set a background image for a worksheet. + + Args: + filename: Path and filename for in supported formats. + is_byte_stream: File is a stream of bytes. + + Returns: + 0: Success. + -1: Image file not found. + + """ + + if not is_byte_stream and not os.path.exists(filename): + warn(f"Image file '{filename}' not found.") + return -1 + + self.background_bytes = is_byte_stream + self.background_image = filename + + return 0 + + def set_comments_author(self, author): + """ + Set the default author of the cell comments. + + Args: + author: Comment author name. String. + + Returns: + Nothing. + + """ + self.comments_author = author + + def get_name(self): + """ + Retrieve the worksheet name. + + Args: + None. + + Returns: + Nothing. + + """ + # There is no set_name() method. Name must be set in add_worksheet(). + return self.name + + def activate(self): + """ + Set this worksheet as the active worksheet, i.e. the worksheet that is + displayed when the workbook is opened. Also set it as selected. + + Note: An active worksheet cannot be hidden. + + Args: + None. + + Returns: + Nothing. + + """ + self.hidden = 0 + self.selected = 1 + self.worksheet_meta.activesheet = self.index + + def select(self): + """ + Set current worksheet as a selected worksheet, i.e. the worksheet + has its tab highlighted. + + Note: A selected worksheet cannot be hidden. + + Args: + None. + + Returns: + Nothing. + + """ + self.selected = 1 + self.hidden = 0 + + def hide(self): + """ + Hide the current worksheet. + + Args: + None. + + Returns: + Nothing. + + """ + self.hidden = 1 + + # A hidden worksheet shouldn't be active or selected. + self.selected = 0 + + def very_hidden(self): + """ + Hide the current worksheet. This can only be unhidden by VBA. + + Args: + None. + + Returns: + Nothing. + + """ + self.hidden = 2 + + # A hidden worksheet shouldn't be active or selected. + self.selected = 0 + + def set_first_sheet(self): + """ + Set current worksheet as the first visible sheet. This is necessary + when there are a large number of worksheets and the activated + worksheet is not visible on the screen. + + Note: A selected worksheet cannot be hidden. + + Args: + None. + + Returns: + Nothing. + + """ + self.hidden = 0 # Active worksheet can't be hidden. + self.worksheet_meta.firstsheet = self.index + + @convert_column_args + def set_column( + self, first_col, last_col, width=None, cell_format=None, options=None + ): + """ + Set the width, and other properties of a single column or a + range of columns. + + Args: + first_col: First column (zero-indexed). + last_col: Last column (zero-indexed). Can be same as first_col. + width: Column width. (optional). + cell_format: Column cell_format. (optional). + options: Dict of options such as hidden and level. + + Returns: + 0: Success. + -1: Column number is out of worksheet bounds. + + """ + if options is None: + options = {} + + # Ensure 2nd col is larger than first. + if first_col > last_col: + (first_col, last_col) = (last_col, first_col) + + # Don't modify the row dimensions when checking the columns. + ignore_row = True + + # Set optional column values. + hidden = options.get("hidden", False) + collapsed = options.get("collapsed", False) + level = options.get("level", 0) + + # Store the column dimension only in some conditions. + if cell_format or (width and hidden): + ignore_col = False + else: + ignore_col = True + + # Check that each column is valid and store the max and min values. + if self._check_dimensions(0, last_col, ignore_row, ignore_col): + return -1 + if self._check_dimensions(0, first_col, ignore_row, ignore_col): + return -1 + + # Set the limits for the outline levels (0 <= x <= 7). + level = max(level, 0) + level = min(level, 7) + + self.outline_col_level = max(self.outline_col_level, level) + + # Store the column data. + for col in range(first_col, last_col + 1): + self.col_info[col] = [width, cell_format, hidden, level, collapsed, False] + + # Store the column change to allow optimizations. + self.col_size_changed = True + + return 0 + + @convert_column_args + def set_column_pixels( + self, first_col, last_col, width=None, cell_format=None, options=None + ): + """ + Set the width, and other properties of a single column or a + range of columns, where column width is in pixels. + + Args: + first_col: First column (zero-indexed). + last_col: Last column (zero-indexed). Can be same as first_col. + width: Column width in pixels. (optional). + cell_format: Column cell_format. (optional). + options: Dict of options such as hidden and level. + + Returns: + 0: Success. + -1: Column number is out of worksheet bounds. + + """ + if width is not None: + width = self._pixels_to_width(width) + + return self.set_column(first_col, last_col, width, cell_format, options) + + def autofit(self, max_width=1790): + """ + Simulate autofit based on the data, and datatypes in each column. + + Args: + max_width (optional): max column width to autofit, in pixels. + + Returns: + Nothing. + + """ + # pylint: disable=too-many-nested-blocks + if self.constant_memory: + warn("Autofit is not supported in constant_memory mode.") + return + + # No data written to the target sheet; nothing to autofit + if self.dim_rowmax is None: + return + + # Store the max pixel width for each column. + col_width_max = {} + + # Convert the autofit maximum pixel width to a column/character width, + # but limit it to the Excel max limit. + max_width = min(self._pixels_to_width(max_width), 255.0) + + # Create a reverse lookup for the share strings table so we can convert + # the string id back to the original string. + strings = sorted( + self.str_table.string_table, key=self.str_table.string_table.__getitem__ + ) + + for row_num in range(self.dim_rowmin, self.dim_rowmax + 1): + if not self.table.get(row_num): + continue + + for col_num in range(self.dim_colmin, self.dim_colmax + 1): + if col_num in self.table[row_num]: + cell = self.table[row_num][col_num] + cell_type = cell.__class__.__name__ + length = 0 + + if cell_type in ("String", "RichString"): + # Handle strings and rich strings. + # + # For standard shared strings we do a reverse lookup + # from the shared string id to the actual string. For + # rich strings we use the unformatted string. We also + # split multi-line strings and handle each part + # separately. + if cell_type == "String": + string_id = cell.string + string = strings[string_id] + else: + string = cell.raw_string + + if "\n" not in string: + # Single line string. + length = xl_pixel_width(string) + else: + # Handle multi-line strings. + for string in string.split("\n"): + seg_length = xl_pixel_width(string) + length = max(length, seg_length) + + elif cell_type == "Number": + # Handle numbers. + # + # We use a workaround/optimization for numbers since + # digits all have a pixel width of 7. This gives a + # slightly greater width for the decimal place and + # minus sign but only by a few pixels and + # over-estimation is okay. + length = 7 * len(str(cell.number)) + + elif cell_type == "Datetime": + # Handle dates. + # + # The following uses the default width for mm/dd/yyyy + # dates. It isn't feasible to parse the number format + # to get the actual string width for all format types. + length = self.default_date_pixels + + elif cell_type == "Boolean": + # Handle boolean values. + # + # Use the Excel standard widths for TRUE and FALSE. + if cell.boolean: + length = 31 + else: + length = 36 + + elif cell_type in ("Formula", "ArrayFormula"): + # Handle formulas. + # + # We only try to autofit a formula if it has a + # non-zero value. + if isinstance(cell.value, (float, int)): + if cell.value > 0: + length = 7 * len(str(cell.value)) + + elif isinstance(cell.value, str): + length = xl_pixel_width(cell.value) + + elif isinstance(cell.value, bool): + if cell.value: + length = 31 + else: + length = 36 + + # If the cell is in an autofilter header we add an + # additional 16 pixels for the dropdown arrow. + if self.filter_cells.get((row_num, col_num)) and length > 0: + length += 16 + + # Add the string length to the lookup table. + width_max = col_width_max.get(col_num, 0) + if length > width_max: + col_width_max[col_num] = length + + # Apply the width to the column. + for col_num, pixel_width in col_width_max.items(): + # Convert the string pixel width to a character width using an + # additional padding of 7 pixels, like Excel. + width = self._pixels_to_width(pixel_width + 7) + + # Limit the width to the maximum user or Excel value. + width = min(width, max_width) + + # Add the width to an existing col info structure or add a new one. + if self.col_info.get(col_num): + # We only update the width for an existing column if it is + # greater than the user defined value. This allows the user + # to pre-load a minimum col width. + col_info = self.col_info.get(col_num) + user_width = col_info[0] + hidden = col_info[5] + if user_width is not None and not hidden: + # Col info is user defined. + if width > user_width: + self.col_info[col_num][0] = width + self.col_info[col_num][5] = True + else: + self.col_info[col_num][0] = width + self.col_info[col_num][5] = True + else: + self.col_info[col_num] = [width, None, False, 0, False, True] + + def set_row(self, row, height=None, cell_format=None, options=None): + """ + Set the width, and other properties of a row. + + Args: + row: Row number (zero-indexed). + height: Row height. (optional). + cell_format: Row cell_format. (optional). + options: Dict of options such as hidden, level and collapsed. + + Returns: + 0: Success. + -1: Row number is out of worksheet bounds. + + """ + if options is None: + options = {} + + # Use minimum col in _check_dimensions(). + if self.dim_colmin is not None: + min_col = self.dim_colmin + else: + min_col = 0 + + # Check that row is valid. + if self._check_dimensions(row, min_col): + return -1 + + if height is None: + height = self.default_row_height + + # Set optional row values. + hidden = options.get("hidden", False) + collapsed = options.get("collapsed", False) + level = options.get("level", 0) + + # If the height is 0 the row is hidden and the height is the default. + if height == 0: + hidden = 1 + height = self.default_row_height + + # Set the limits for the outline levels (0 <= x <= 7). + level = max(level, 0) + level = min(level, 7) + + self.outline_row_level = max(self.outline_row_level, level) + + # Store the row properties. + self.set_rows[row] = [height, cell_format, hidden, level, collapsed] + + # Store the row change to allow optimizations. + self.row_size_changed = True + + # Store the row sizes for use when calculating image vertices. + self.row_sizes[row] = [height, hidden] + + return 0 + + def set_row_pixels(self, row, height=None, cell_format=None, options=None): + """ + Set the width (in pixels), and other properties of a row. + + Args: + row: Row number (zero-indexed). + height: Row height in pixels. (optional). + cell_format: Row cell_format. (optional). + options: Dict of options such as hidden, level and collapsed. + + Returns: + 0: Success. + -1: Row number is out of worksheet bounds. + + """ + if height is not None: + height = self._pixels_to_height(height) + + return self.set_row(row, height, cell_format, options) + + def set_default_row(self, height=None, hide_unused_rows=False): + """ + Set the default row properties. + + Args: + height: Default height. Optional, defaults to 15. + hide_unused_rows: Hide unused rows. Optional, defaults to False. + + Returns: + Nothing. + + """ + if height is None: + height = self.default_row_height + + if height != self.original_row_height: + # Store the row change to allow optimizations. + self.row_size_changed = True + self.default_row_height = height + + if hide_unused_rows: + self.default_row_zeroed = 1 + + @convert_range_args + def merge_range( + self, first_row, first_col, last_row, last_col, data, cell_format=None + ): + """ + Merge a range of cells. + + Args: + first_row: The first row of the cell range. (zero indexed). + first_col: The first column of the cell range. + last_row: The last row of the cell range. (zero indexed). + last_col: The last column of the cell range. + data: Cell data. + cell_format: Cell Format object. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + other: Return value of write(). + + """ + # Merge a range of cells. The first cell should contain the data and + # the others should be blank. All cells should have the same format. + + # Excel doesn't allow a single cell to be merged + if first_row == last_row and first_col == last_col: + warn("Can't merge single cell") + return -1 + + # Swap last row/col with first row/col as necessary + if first_row > last_row: + (first_row, last_row) = (last_row, first_row) + if first_col > last_col: + (first_col, last_col) = (last_col, first_col) + + # Check that row and col are valid and store max and min values. + if self._check_dimensions(first_row, first_col): + return -1 + if self._check_dimensions(last_row, last_col): + return -1 + + # Check if the merge range overlaps a previous merged or table range. + # This is a critical file corruption error in Excel. + cell_range = xl_range(first_row, first_col, last_row, last_col) + for row in range(first_row, last_row + 1): + for col in range(first_col, last_col + 1): + if self.merged_cells.get((row, col)): + previous_range = self.merged_cells.get((row, col)) + raise OverlappingRange( + f"Merge range '{cell_range}' overlaps previous merge " + f"range '{previous_range}'." + ) + + if self.table_cells.get((row, col)): + previous_range = self.table_cells.get((row, col)) + raise OverlappingRange( + f"Merge range '{cell_range}' overlaps previous table " + f"range '{previous_range}'." + ) + + self.merged_cells[(row, col)] = cell_range + + # Store the merge range. + self.merge.append([first_row, first_col, last_row, last_col]) + + # Write the first cell + self._write(first_row, first_col, data, cell_format) + + # Pad out the rest of the area with formatted blank cells. + for row in range(first_row, last_row + 1): + for col in range(first_col, last_col + 1): + if row == first_row and col == first_col: + continue + self._write_blank(row, col, "", cell_format) + + return 0 + + @convert_range_args + def autofilter(self, first_row, first_col, last_row, last_col): + """ + Set the autofilter area in the worksheet. + + Args: + first_row: The first row of the cell range. (zero indexed). + first_col: The first column of the cell range. + last_row: The last row of the cell range. (zero indexed). + last_col: The last column of the cell range. + + Returns: + Nothing. + + """ + # Reverse max and min values if necessary. + if last_row < first_row: + (first_row, last_row) = (last_row, first_row) + if last_col < first_col: + (first_col, last_col) = (last_col, first_col) + + # Build up the autofilter area range "Sheet1!$A$1:$C$13". + area = self._convert_name_area(first_row, first_col, last_row, last_col) + ref = xl_range(first_row, first_col, last_row, last_col) + + self.autofilter_area = area + self.autofilter_ref = ref + self.filter_range = [first_col, last_col] + + # Store the filter cell positions for use in the autofit calculation. + for col in range(first_col, last_col + 1): + # Check that the autofilter doesn't overlap a table filter. + if self.filter_cells.get((first_row, col)): + filter_type, filter_range = self.filter_cells.get((first_row, col)) + if filter_type == "table": + raise OverlappingRange( + f"Worksheet autofilter range '{ref}' overlaps previous " + f"Table autofilter range '{filter_range}'." + ) + + self.filter_cells[(first_row, col)] = ("worksheet", ref) + + def filter_column(self, col, criteria): + """ + Set the column filter criteria. + + Args: + col: Filter column (zero-indexed). + criteria: Filter criteria. + + Returns: + Nothing. + + """ + if not self.autofilter_area: + warn("Must call autofilter() before filter_column()") + return + + # Check for a column reference in A1 notation and substitute. + try: + int(col) + except ValueError: + # Convert col ref to a cell ref and then to a col number. + col_letter = col + (_, col) = xl_cell_to_rowcol(col + "1") + + if col >= self.xls_colmax: + warn(f"Invalid column '{col_letter}'") + return + + (col_first, col_last) = self.filter_range + + # Reject column if it is outside filter range. + if col < col_first or col > col_last: + warn( + f"Column '{col}' outside autofilter() column " + f"range ({col_first}, {col_last})" + ) + return + + tokens = self._extract_filter_tokens(criteria) + + if len(tokens) not in (3, 7): + warn(f"Incorrect number of tokens in criteria '{criteria}'") + + tokens = self._parse_filter_expression(criteria, tokens) + + # Excel handles single or double custom filters as default filters. + # We need to check for them and handle them accordingly. + if len(tokens) == 2 and tokens[0] == 2: + # Single equality. + self.filter_column_list(col, [tokens[1]]) + elif len(tokens) == 5 and tokens[0] == 2 and tokens[2] == 1 and tokens[3] == 2: + # Double equality with "or" operator. + self.filter_column_list(col, [tokens[1], tokens[4]]) + else: + # Non default custom filter. + self.filter_cols[col] = tokens + self.filter_type[col] = 0 + + self.filter_on = 1 + + def filter_column_list(self, col, filters): + """ + Set the column filter criteria in Excel 2007 list style. + + Args: + col: Filter column (zero-indexed). + filters: List of filter criteria to match. + + Returns: + Nothing. + + """ + if not self.autofilter_area: + warn("Must call autofilter() before filter_column()") + return + + # Check for a column reference in A1 notation and substitute. + try: + int(col) + except ValueError: + # Convert col ref to a cell ref and then to a col number. + col_letter = col + (_, col) = xl_cell_to_rowcol(col + "1") + + if col >= self.xls_colmax: + warn(f"Invalid column '{col_letter}'") + return + + (col_first, col_last) = self.filter_range + + # Reject column if it is outside filter range. + if col < col_first or col > col_last: + warn( + f"Column '{col}' outside autofilter() column range " + f"({col_first},{col_last})" + ) + return + + self.filter_cols[col] = filters + self.filter_type[col] = 1 + self.filter_on = 1 + + @convert_range_args + def data_validation(self, first_row, first_col, last_row, last_col, options=None): + """ + Add a data validation to a worksheet. + + Args: + first_row: The first row of the cell range. (zero indexed). + first_col: The first column of the cell range. + last_row: The last row of the cell range. (zero indexed). + last_col: The last column of the cell range. + options: Data validation options. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + -2: Incorrect parameter or option. + """ + # Check that row and col are valid without storing the values. + if self._check_dimensions(first_row, first_col, True, True): + return -1 + if self._check_dimensions(last_row, last_col, True, True): + return -1 + + if options is None: + options = {} + else: + # Copy the user defined options so they aren't modified. + options = options.copy() + + # Valid input parameters. + valid_parameters = { + "validate", + "criteria", + "value", + "source", + "minimum", + "maximum", + "ignore_blank", + "dropdown", + "show_input", + "input_title", + "input_message", + "show_error", + "error_title", + "error_message", + "error_type", + "other_cells", + "multi_range", + } + + # Check for valid input parameters. + for param_key in options.keys(): + if param_key not in valid_parameters: + warn(f"Unknown parameter '{param_key}' in data_validation()") + return -2 + + # Map alternative parameter names 'source' or 'minimum' to 'value'. + if "source" in options: + options["value"] = options["source"] + if "minimum" in options: + options["value"] = options["minimum"] + + # 'validate' is a required parameter. + if "validate" not in options: + warn("Parameter 'validate' is required in data_validation()") + return -2 + + # List of valid validation types. + valid_types = { + "any": "none", + "any value": "none", + "whole number": "whole", + "whole": "whole", + "integer": "whole", + "decimal": "decimal", + "list": "list", + "date": "date", + "time": "time", + "text length": "textLength", + "length": "textLength", + "custom": "custom", + } + + # Check for valid validation types. + if options["validate"] not in valid_types: + warn( + f"Unknown validation type '{options['validate']}' for parameter " + f"'validate' in data_validation()" + ) + return -2 + + options["validate"] = valid_types[options["validate"]] + + # No action is required for validation type 'any' if there are no + # input messages to display. + if ( + options["validate"] == "none" + and options.get("input_title") is None + and options.get("input_message") is None + ): + return -2 + + # The any, list and custom validations don't have a criteria so we use + # a default of 'between'. + if ( + options["validate"] == "none" + or options["validate"] == "list" + or options["validate"] == "custom" + ): + options["criteria"] = "between" + options["maximum"] = None + + # 'criteria' is a required parameter. + if "criteria" not in options: + warn("Parameter 'criteria' is required in data_validation()") + return -2 + + # Valid criteria types. + criteria_types = { + "between": "between", + "not between": "notBetween", + "equal to": "equal", + "=": "equal", + "==": "equal", + "not equal to": "notEqual", + "!=": "notEqual", + "<>": "notEqual", + "greater than": "greaterThan", + ">": "greaterThan", + "less than": "lessThan", + "<": "lessThan", + "greater than or equal to": "greaterThanOrEqual", + ">=": "greaterThanOrEqual", + "less than or equal to": "lessThanOrEqual", + "<=": "lessThanOrEqual", + } + + # Check for valid criteria types. + if options["criteria"] not in criteria_types: + warn( + f"Unknown criteria type '{options['criteria']}' for parameter " + f"'criteria' in data_validation()" + ) + return -2 + + options["criteria"] = criteria_types[options["criteria"]] + + # 'Between' and 'Not between' criteria require 2 values. + if options["criteria"] == "between" or options["criteria"] == "notBetween": + if "maximum" not in options: + warn( + "Parameter 'maximum' is required in data_validation() " + "when using 'between' or 'not between' criteria" + ) + return -2 + else: + options["maximum"] = None + + # Valid error dialog types. + error_types = { + "stop": 0, + "warning": 1, + "information": 2, + } + + # Check for valid error dialog types. + if "error_type" not in options: + options["error_type"] = 0 + elif options["error_type"] not in error_types: + warn( + f"Unknown criteria type '{options['error_type']}' " + f"for parameter 'error_type'." + ) + return -2 + else: + options["error_type"] = error_types[options["error_type"]] + + # Convert date/times value if required. + if ( + options["validate"] in ("date", "time") + and options["value"] + and _supported_datetime(options["value"]) + ): + date_time = self._convert_date_time(options["value"]) + # Format date number to the same precision as Excel. + options["value"] = f"{date_time:.16g}" + + if options["maximum"] and _supported_datetime(options["maximum"]): + date_time = self._convert_date_time(options["maximum"]) + options["maximum"] = f"{date_time:.16g}" + + # Check that the input title doesn't exceed the maximum length. + if options.get("input_title") and len(options["input_title"]) > 32: + warn( + f"Length of input title '{options['input_title']}' " + f"exceeds Excel's limit of 32" + ) + return -2 + + # Check that the error title doesn't exceed the maximum length. + if options.get("error_title") and len(options["error_title"]) > 32: + warn( + f"Length of error title '{options['error_title']}' " + f"exceeds Excel's limit of 32" + ) + return -2 + + # Check that the input message doesn't exceed the maximum length. + if options.get("input_message") and len(options["input_message"]) > 255: + warn( + f"Length of input message '{options['input_message']}' " + f"exceeds Excel's limit of 255" + ) + return -2 + + # Check that the error message doesn't exceed the maximum length. + if options.get("error_message") and len(options["error_message"]) > 255: + warn( + f"Length of error message '{options['error_message']}' " + f"exceeds Excel's limit of 255" + ) + return -2 + + # Check that the input list doesn't exceed the maximum length. + if options["validate"] == "list" and isinstance(options["value"], list): + formula = self._csv_join(*options["value"]) + if len(formula) > 255: + warn( + f"Length of list items '{formula}' exceeds Excel's limit of " + f"255, use a formula range instead" + ) + return -2 + + # Set some defaults if they haven't been defined by the user. + if "ignore_blank" not in options: + options["ignore_blank"] = 1 + if "dropdown" not in options: + options["dropdown"] = 1 + if "show_input" not in options: + options["show_input"] = 1 + if "show_error" not in options: + options["show_error"] = 1 + + # These are the cells to which the validation is applied. + options["cells"] = [[first_row, first_col, last_row, last_col]] + + # A (for now) undocumented parameter to pass additional cell ranges. + if "other_cells" in options: + options["cells"].extend(options["other_cells"]) + + # Override with user defined multiple range if provided. + if "multi_range" in options: + options["multi_range"] = options["multi_range"].replace("$", "") + + # Store the validation information until we close the worksheet. + self.validations.append(options) + + return 0 + + @convert_range_args + def conditional_format( + self, first_row, first_col, last_row, last_col, options=None + ): + """ + Add a conditional format to a worksheet. + + Args: + first_row: The first row of the cell range. (zero indexed). + first_col: The first column of the cell range. + last_row: The last row of the cell range. (zero indexed). + last_col: The last column of the cell range. + options: Conditional format options. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + -2: Incorrect parameter or option. + """ + # Check that row and col are valid without storing the values. + if self._check_dimensions(first_row, first_col, True, True): + return -1 + if self._check_dimensions(last_row, last_col, True, True): + return -1 + + if options is None: + options = {} + else: + # Copy the user defined options so they aren't modified. + options = options.copy() + + # Valid input parameters. + valid_parameter = { + "type", + "format", + "criteria", + "value", + "minimum", + "maximum", + "stop_if_true", + "min_type", + "mid_type", + "max_type", + "min_value", + "mid_value", + "max_value", + "min_color", + "mid_color", + "max_color", + "min_length", + "max_length", + "multi_range", + "bar_color", + "bar_negative_color", + "bar_negative_color_same", + "bar_solid", + "bar_border_color", + "bar_negative_border_color", + "bar_negative_border_color_same", + "bar_no_border", + "bar_direction", + "bar_axis_position", + "bar_axis_color", + "bar_only", + "data_bar_2010", + "icon_style", + "reverse_icons", + "icons_only", + "icons", + } + + # Check for valid input parameters. + for param_key in options.keys(): + if param_key not in valid_parameter: + warn(f"Unknown parameter '{param_key}' in conditional_format()") + return -2 + + # 'type' is a required parameter. + if "type" not in options: + warn("Parameter 'type' is required in conditional_format()") + return -2 + + # Valid types. + valid_type = { + "cell": "cellIs", + "date": "date", + "time": "time", + "average": "aboveAverage", + "duplicate": "duplicateValues", + "unique": "uniqueValues", + "top": "top10", + "bottom": "top10", + "text": "text", + "time_period": "timePeriod", + "blanks": "containsBlanks", + "no_blanks": "notContainsBlanks", + "errors": "containsErrors", + "no_errors": "notContainsErrors", + "2_color_scale": "2_color_scale", + "3_color_scale": "3_color_scale", + "data_bar": "dataBar", + "formula": "expression", + "icon_set": "iconSet", + } + + # Check for valid types. + if options["type"] not in valid_type: + warn( + f"Unknown value '{options['type']}' for parameter 'type' " + f"in conditional_format()" + ) + return -2 + + if options["type"] == "bottom": + options["direction"] = "bottom" + options["type"] = valid_type[options["type"]] + + # Valid criteria types. + criteria_type = { + "between": "between", + "not between": "notBetween", + "equal to": "equal", + "=": "equal", + "==": "equal", + "not equal to": "notEqual", + "!=": "notEqual", + "<>": "notEqual", + "greater than": "greaterThan", + ">": "greaterThan", + "less than": "lessThan", + "<": "lessThan", + "greater than or equal to": "greaterThanOrEqual", + ">=": "greaterThanOrEqual", + "less than or equal to": "lessThanOrEqual", + "<=": "lessThanOrEqual", + "containing": "containsText", + "not containing": "notContains", + "begins with": "beginsWith", + "ends with": "endsWith", + "yesterday": "yesterday", + "today": "today", + "last 7 days": "last7Days", + "last week": "lastWeek", + "this week": "thisWeek", + "next week": "nextWeek", + "last month": "lastMonth", + "this month": "thisMonth", + "next month": "nextMonth", + # For legacy, but incorrect, support. + "continue week": "nextWeek", + "continue month": "nextMonth", + } + + # Check for valid criteria types. + if "criteria" in options and options["criteria"] in criteria_type: + options["criteria"] = criteria_type[options["criteria"]] + + # Convert boolean values if required. + if "value" in options and isinstance(options["value"], bool): + options["value"] = str(options["value"]).upper() + + # Convert date/times value if required. + if options["type"] in ("date", "time"): + options["type"] = "cellIs" + + if "value" in options: + if not _supported_datetime(options["value"]): + warn("Conditional format 'value' must be a datetime object.") + return -2 + + date_time = self._convert_date_time(options["value"]) + # Format date number to the same precision as Excel. + options["value"] = f"{date_time:.16g}" + + if "minimum" in options: + if not _supported_datetime(options["minimum"]): + warn("Conditional format 'minimum' must be a datetime object.") + return -2 + + date_time = self._convert_date_time(options["minimum"]) + options["minimum"] = f"{date_time:.16g}" + + if "maximum" in options: + if not _supported_datetime(options["maximum"]): + warn("Conditional format 'maximum' must be a datetime object.") + return -2 + + date_time = self._convert_date_time(options["maximum"]) + options["maximum"] = f"{date_time:.16g}" + + # Valid icon styles. + valid_icons = { + "3_arrows": "3Arrows", # 1 + "3_flags": "3Flags", # 2 + "3_traffic_lights_rimmed": "3TrafficLights2", # 3 + "3_symbols_circled": "3Symbols", # 4 + "4_arrows": "4Arrows", # 5 + "4_red_to_black": "4RedToBlack", # 6 + "4_traffic_lights": "4TrafficLights", # 7 + "5_arrows_gray": "5ArrowsGray", # 8 + "5_quarters": "5Quarters", # 9 + "3_arrows_gray": "3ArrowsGray", # 10 + "3_traffic_lights": "3TrafficLights", # 11 + "3_signs": "3Signs", # 12 + "3_symbols": "3Symbols2", # 13 + "4_arrows_gray": "4ArrowsGray", # 14 + "4_ratings": "4Rating", # 15 + "5_arrows": "5Arrows", # 16 + "5_ratings": "5Rating", + } # 17 + + # Set the icon set properties. + if options["type"] == "iconSet": + # An icon_set must have an icon style. + if not options.get("icon_style"): + warn( + "The 'icon_style' parameter must be specified when " + "'type' == 'icon_set' in conditional_format()." + ) + return -3 + + # Check for valid icon styles. + if options["icon_style"] not in valid_icons: + warn( + f"Unknown icon_style '{options['icon_style']}' " + f"in conditional_format()." + ) + return -2 + + options["icon_style"] = valid_icons[options["icon_style"]] + + # Set the number of icons for the icon style. + options["total_icons"] = 3 + if options["icon_style"].startswith("4"): + options["total_icons"] = 4 + elif options["icon_style"].startswith("5"): + options["total_icons"] = 5 + + options["icons"] = self._set_icon_props( + options.get("total_icons"), options.get("icons") + ) + + # Swap last row/col for first row/col as necessary + if first_row > last_row: + first_row, last_row = last_row, first_row + + if first_col > last_col: + first_col, last_col = last_col, first_col + + # Set the formatting range. + cell_range = xl_range(first_row, first_col, last_row, last_col) + start_cell = xl_rowcol_to_cell(first_row, first_col) + + # Override with user defined multiple range if provided. + if "multi_range" in options: + cell_range = options["multi_range"] + cell_range = cell_range.replace("$", "") + + # Get the dxf format index. + if "format" in options and options["format"]: + options["format"] = options["format"]._get_dxf_index() + + # Set the priority based on the order of adding. + options["priority"] = self.dxf_priority + self.dxf_priority += 1 + + # Check for 2010 style data_bar parameters. + # pylint: disable=too-many-boolean-expressions + if ( + self.use_data_bars_2010 + or options.get("data_bar_2010") + or options.get("bar_solid") + or options.get("bar_border_color") + or options.get("bar_negative_color") + or options.get("bar_negative_color_same") + or options.get("bar_negative_border_color") + or options.get("bar_negative_border_color_same") + or options.get("bar_no_border") + or options.get("bar_axis_position") + or options.get("bar_axis_color") + or options.get("bar_direction") + ): + options["is_data_bar_2010"] = True + + # Special handling of text criteria. + if options["type"] == "text": + value = options["value"] + length = len(value) + criteria = options["criteria"] + + if options["criteria"] == "containsText": + options["type"] = "containsText" + options["formula"] = f'NOT(ISERROR(SEARCH("{value}",{start_cell})))' + elif options["criteria"] == "notContains": + options["type"] = "notContainsText" + options["formula"] = f'ISERROR(SEARCH("{value}",{start_cell}))' + elif options["criteria"] == "beginsWith": + options["type"] = "beginsWith" + options["formula"] = f'LEFT({start_cell},{length})="{value}"' + elif options["criteria"] == "endsWith": + options["type"] = "endsWith" + options["formula"] = f'RIGHT({start_cell},{length})="{value}"' + else: + warn(f"Invalid text criteria '{criteria}' in conditional_format()") + + # Special handling of time time_period criteria. + if options["type"] == "timePeriod": + if options["criteria"] == "yesterday": + options["formula"] = f"FLOOR({start_cell},1)=TODAY()-1" + + elif options["criteria"] == "today": + options["formula"] = f"FLOOR({start_cell},1)=TODAY()" + + elif options["criteria"] == "tomorrow": + options["formula"] = f"FLOOR({start_cell},1)=TODAY()+1" + + # fmt: off + elif options["criteria"] == "last7Days": + options["formula"] = ( + f"AND(TODAY()-FLOOR({start_cell},1)<=6," + f"FLOOR({start_cell},1)<=TODAY())" + ) + # fmt: on + + elif options["criteria"] == "lastWeek": + options["formula"] = ( + f"AND(TODAY()-ROUNDDOWN({start_cell},0)>=(WEEKDAY(TODAY()))," + f"TODAY()-ROUNDDOWN({start_cell},0)<(WEEKDAY(TODAY())+7))" + ) + + elif options["criteria"] == "thisWeek": + options["formula"] = ( + f"AND(TODAY()-ROUNDDOWN({start_cell},0)<=WEEKDAY(TODAY())-1," + f"ROUNDDOWN({start_cell},0)-TODAY()<=7-WEEKDAY(TODAY()))" + ) + + elif options["criteria"] == "nextWeek": + options["formula"] = ( + f"AND(ROUNDDOWN({start_cell},0)-TODAY()>(7-WEEKDAY(TODAY()))," + f"ROUNDDOWN({start_cell},0)-TODAY()<(15-WEEKDAY(TODAY())))" + ) + + elif options["criteria"] == "lastMonth": + options["formula"] = ( + f"AND(MONTH({start_cell})=MONTH(TODAY())-1," + f"OR(YEAR({start_cell})=YEAR(" + f"TODAY()),AND(MONTH({start_cell})=1,YEAR(A1)=YEAR(TODAY())-1)))" + ) + + # fmt: off + elif options["criteria"] == "thisMonth": + options["formula"] = ( + f"AND(MONTH({start_cell})=MONTH(TODAY())," + f"YEAR({start_cell})=YEAR(TODAY()))" + ) + # fmt: on + + elif options["criteria"] == "nextMonth": + options["formula"] = ( + f"AND(MONTH({start_cell})=MONTH(TODAY())+1," + f"OR(YEAR({start_cell})=YEAR(" + f"TODAY()),AND(MONTH({start_cell})=12," + f"YEAR({start_cell})=YEAR(TODAY())+1)))" + ) + + else: + warn( + f"Invalid time_period criteria '{options['criteria']}' " + f"in conditional_format()" + ) + + # Special handling of blanks/error types. + if options["type"] == "containsBlanks": + options["formula"] = f"LEN(TRIM({start_cell}))=0" + + if options["type"] == "notContainsBlanks": + options["formula"] = f"LEN(TRIM({start_cell}))>0" + + if options["type"] == "containsErrors": + options["formula"] = f"ISERROR({start_cell})" + + if options["type"] == "notContainsErrors": + options["formula"] = f"NOT(ISERROR({start_cell}))" + + # Special handling for 2 color scale. + if options["type"] == "2_color_scale": + options["type"] = "colorScale" + + # Color scales don't use any additional formatting. + options["format"] = None + + # Turn off 3 color parameters. + options["mid_type"] = None + options["mid_color"] = None + + options.setdefault("min_type", "min") + options.setdefault("max_type", "max") + options.setdefault("min_value", 0) + options.setdefault("max_value", 0) + options.setdefault("min_color", "#FF7128") + options.setdefault("max_color", "#FFEF9C") + + options["min_color"] = _xl_color(options["min_color"]) + options["max_color"] = _xl_color(options["max_color"]) + + # Special handling for 3 color scale. + if options["type"] == "3_color_scale": + options["type"] = "colorScale" + + # Color scales don't use any additional formatting. + options["format"] = None + + options.setdefault("min_type", "min") + options.setdefault("mid_type", "percentile") + options.setdefault("max_type", "max") + options.setdefault("min_value", 0) + options.setdefault("max_value", 0) + options.setdefault("min_color", "#F8696B") + options.setdefault("mid_color", "#FFEB84") + options.setdefault("max_color", "#63BE7B") + + options["min_color"] = _xl_color(options["min_color"]) + options["mid_color"] = _xl_color(options["mid_color"]) + options["max_color"] = _xl_color(options["max_color"]) + + # Set a default mid value. + if "mid_value" not in options: + options["mid_value"] = 50 + + # Special handling for data bar. + if options["type"] == "dataBar": + # Color scales don't use any additional formatting. + options["format"] = None + + if not options.get("min_type"): + options["min_type"] = "min" + options["x14_min_type"] = "autoMin" + else: + options["x14_min_type"] = options["min_type"] + + if not options.get("max_type"): + options["max_type"] = "max" + options["x14_max_type"] = "autoMax" + else: + options["x14_max_type"] = options["max_type"] + + options.setdefault("min_value", 0) + options.setdefault("max_value", 0) + options.setdefault("bar_color", "#638EC6") + options.setdefault("bar_border_color", options["bar_color"]) + options.setdefault("bar_only", False) + options.setdefault("bar_no_border", False) + options.setdefault("bar_solid", False) + options.setdefault("bar_direction", "") + options.setdefault("bar_negative_color", "#FF0000") + options.setdefault("bar_negative_border_color", "#FF0000") + options.setdefault("bar_negative_color_same", False) + options.setdefault("bar_negative_border_color_same", False) + options.setdefault("bar_axis_position", "") + options.setdefault("bar_axis_color", "#000000") + + options["bar_color"] = _xl_color(options["bar_color"]) + options["bar_border_color"] = _xl_color(options["bar_border_color"]) + options["bar_axis_color"] = _xl_color(options["bar_axis_color"]) + options["bar_negative_color"] = _xl_color(options["bar_negative_color"]) + options["bar_negative_border_color"] = _xl_color( + options["bar_negative_border_color"] + ) + + # Adjust for 2010 style data_bar parameters. + if options.get("is_data_bar_2010"): + self.excel_version = 2010 + + if options["min_type"] == "min" and options["min_value"] == 0: + options["min_value"] = None + + if options["max_type"] == "max" and options["max_value"] == 0: + options["max_value"] = None + + options["range"] = cell_range + + # Strip the leading = from formulas. + try: + options["min_value"] = options["min_value"].lstrip("=") + except (KeyError, AttributeError): + pass + try: + options["mid_value"] = options["mid_value"].lstrip("=") + except (KeyError, AttributeError): + pass + try: + options["max_value"] = options["max_value"].lstrip("=") + except (KeyError, AttributeError): + pass + + # Store the conditional format until we close the worksheet. + if cell_range in self.cond_formats: + self.cond_formats[cell_range].append(options) + else: + self.cond_formats[cell_range] = [options] + + return 0 + + @convert_range_args + def add_table(self, first_row, first_col, last_row, last_col, options=None): + """ + Add an Excel table to a worksheet. + + Args: + first_row: The first row of the cell range. (zero indexed). + first_col: The first column of the cell range. + last_row: The last row of the cell range. (zero indexed). + last_col: The last column of the cell range. + options: Table format options. (Optional) + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + -2: Incorrect parameter or option. + -3: Not supported in constant_memory mode. + """ + table = {} + col_formats = {} + + if options is None: + options = {} + else: + # Copy the user defined options so they aren't modified. + options = options.copy() + + if self.constant_memory: + warn("add_table() isn't supported in 'constant_memory' mode") + return -3 + + # Check that row and col are valid without storing the values. + if self._check_dimensions(first_row, first_col, True, True): + return -1 + if self._check_dimensions(last_row, last_col, True, True): + return -1 + + # Swap last row/col for first row/col as necessary. + if first_row > last_row: + (first_row, last_row) = (last_row, first_row) + if first_col > last_col: + (first_col, last_col) = (last_col, first_col) + + # Check if the table range overlaps a previous merged or table range. + # This is a critical file corruption error in Excel. + cell_range = xl_range(first_row, first_col, last_row, last_col) + for row in range(first_row, last_row + 1): + for col in range(first_col, last_col + 1): + if self.table_cells.get((row, col)): + previous_range = self.table_cells.get((row, col)) + raise OverlappingRange( + f"Table range '{cell_range}' overlaps previous " + f"table range '{previous_range}'." + ) + + if self.merged_cells.get((row, col)): + previous_range = self.merged_cells.get((row, col)) + raise OverlappingRange( + f"Table range '{cell_range}' overlaps previous " + f"merge range '{previous_range}'." + ) + + self.table_cells[(row, col)] = cell_range + + # Valid input parameters. + valid_parameter = { + "autofilter", + "banded_columns", + "banded_rows", + "columns", + "data", + "first_column", + "header_row", + "last_column", + "name", + "style", + "total_row", + } + + # Check for valid input parameters. + for param_key in options.keys(): + if param_key not in valid_parameter: + warn(f"Unknown parameter '{param_key}' in add_table()") + return -2 + + # Turn on Excel's defaults. + options["banded_rows"] = options.get("banded_rows", True) + options["header_row"] = options.get("header_row", True) + options["autofilter"] = options.get("autofilter", True) + + # Check that there are enough rows. + num_rows = last_row - first_row + if options["header_row"]: + num_rows -= 1 + + if num_rows < 0: + warn("Must have at least one data row in in add_table()") + return -2 + + # Set the table options. + table["show_first_col"] = options.get("first_column", False) + table["show_last_col"] = options.get("last_column", False) + table["show_row_stripes"] = options.get("banded_rows", False) + table["show_col_stripes"] = options.get("banded_columns", False) + table["header_row_count"] = options.get("header_row", 0) + table["totals_row_shown"] = options.get("total_row", False) + + # Set the table name. + if "name" in options: + name = options["name"] + table["name"] = name + + if " " in name: + warn(f"Name '{name}' in add_table() cannot contain spaces") + return -2 + + # Warn if the name contains invalid chars as defined by Excel. + if not re.match(r"^[\w\\][\w\\.]*$", name, re.UNICODE) or re.match( + r"^\d", name + ): + warn(f"Invalid Excel characters in add_table(): '{name}'") + return -2 + + # Warn if the name looks like a cell name. + if re.match(r"^[a-zA-Z][a-zA-Z]?[a-dA-D]?\d+$", name): + warn(f"Name looks like a cell name in add_table(): '{name}'") + return -2 + + # Warn if the name looks like a R1C1 cell reference. + if re.match(r"^[rcRC]$", name) or re.match(r"^[rcRC]\d+[rcRC]\d+$", name): + warn(f"Invalid name '{name}' like a RC cell ref in add_table()") + return -2 + + # Set the table style. + if "style" in options: + table["style"] = options["style"] + + if table["style"] is None: + table["style"] = "" + + # Remove whitespace from style name. + table["style"] = table["style"].replace(" ", "") + else: + table["style"] = "TableStyleMedium9" + + # Set the data range rows (without the header and footer). + first_data_row = first_row + last_data_row = last_row + + if options.get("header_row"): + first_data_row += 1 + + if options.get("total_row"): + last_data_row -= 1 + + # Set the table and autofilter ranges. + table["range"] = xl_range(first_row, first_col, last_row, last_col) + + table["a_range"] = xl_range(first_row, first_col, last_data_row, last_col) + + # If the header row if off the default is to turn autofilter off. + if not options["header_row"]: + options["autofilter"] = 0 + + # Set the autofilter range. + if options["autofilter"]: + table["autofilter"] = table["a_range"] + + # Add the table columns. + col_id = 1 + table["columns"] = [] + seen_names = {} + + for col_num in range(first_col, last_col + 1): + # Set up the default column data. + col_data = { + "id": col_id, + "name": "Column" + str(col_id), + "total_string": "", + "total_function": "", + "custom_total": "", + "total_value": 0, + "formula": "", + "format": None, + "name_format": None, + } + + # Overwrite the defaults with any user defined values. + if "columns" in options: + # Check if there are user defined values for this column. + if col_id <= len(options["columns"]): + user_data = options["columns"][col_id - 1] + else: + user_data = None + + if user_data: + # Get the column format. + xformat = user_data.get("format", None) + + # Map user defined values to internal values. + if user_data.get("header"): + col_data["name"] = user_data["header"] + + # Excel requires unique case insensitive header names. + header_name = col_data["name"] + name = header_name.lower() + if name in seen_names: + warn(f"Duplicate header name in add_table(): '{name}'") + return -2 + + seen_names[name] = True + + col_data["name_format"] = user_data.get("header_format") + + # Handle the column formula. + if "formula" in user_data and user_data["formula"]: + formula = user_data["formula"] + + # Remove the formula '=' sign if it exists. + if formula.startswith("="): + formula = formula.lstrip("=") + + # Convert Excel 2010 "@" ref to 2007 "#This Row". + formula = self._prepare_table_formula(formula) + + # Escape any future functions. + formula = self._prepare_formula(formula, True) + + col_data["formula"] = formula + # We write the formulas below after the table data. + + # Handle the function for the total row. + if user_data.get("total_function"): + function = user_data["total_function"] + if function == "count_nums": + function = "countNums" + if function == "std_dev": + function = "stdDev" + + subtotals = set( + [ + "average", + "countNums", + "count", + "max", + "min", + "stdDev", + "sum", + "var", + ] + ) + + if function in subtotals: + formula = self._table_function_to_formula( + function, col_data["name"] + ) + else: + formula = self._prepare_formula(function, True) + col_data["custom_total"] = formula + function = "custom" + + col_data["total_function"] = function + + value = user_data.get("total_value", 0) + + self._write_formula(last_row, col_num, formula, xformat, value) + + elif user_data.get("total_string"): + # Total label only (not a function). + total_string = user_data["total_string"] + col_data["total_string"] = total_string + + self._write_string( + last_row, col_num, total_string, user_data.get("format") + ) + + # Get the dxf format index. + if xformat is not None: + col_data["format"] = xformat._get_dxf_index() + + # Store the column format for writing the cell data. + # It doesn't matter if it is undefined. + col_formats[col_id - 1] = xformat + + # Store the column data. + table["columns"].append(col_data) + + # Write the column headers to the worksheet. + if options["header_row"]: + self._write_string( + first_row, col_num, col_data["name"], col_data["name_format"] + ) + + col_id += 1 + + # Write the cell data if supplied. + if "data" in options: + data = options["data"] + + i = 0 # For indexing the row data. + for row in range(first_data_row, last_data_row + 1): + j = 0 # For indexing the col data. + for col in range(first_col, last_col + 1): + if i < len(data) and j < len(data[i]): + token = data[i][j] + if j in col_formats: + self._write(row, col, token, col_formats[j]) + else: + self._write(row, col, token, None) + j += 1 + i += 1 + + # Write any columns formulas after the user supplied table data to + # overwrite it if required. + for col_id, col_num in enumerate(range(first_col, last_col + 1)): + column_data = table["columns"][col_id] + if column_data and column_data["formula"]: + formula_format = col_formats.get(col_id) + formula = column_data["formula"] + + for row in range(first_data_row, last_data_row + 1): + self._write_formula(row, col_num, formula, formula_format) + + # Store the table data. + self.tables.append(table) + + # Store the filter cell positions for use in the autofit calculation. + if options["autofilter"]: + for col in range(first_col, last_col + 1): + # Check that the table autofilter doesn't overlap a worksheet filter. + if self.filter_cells.get((first_row, col)): + filter_type, filter_range = self.filter_cells.get((first_row, col)) + if filter_type == "worksheet": + raise OverlappingRange( + f"Table autofilter range '{cell_range}' overlaps previous " + f"Worksheet autofilter range '{filter_range}'." + ) + + self.filter_cells[(first_row, col)] = ("table", cell_range) + + return 0 + + @convert_cell_args + def add_sparkline(self, row, col, options=None): + """ + Add sparklines to the worksheet. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + options: Sparkline formatting options. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + -2: Incorrect parameter or option. + + """ + + # Check that row and col are valid without storing the values. + if self._check_dimensions(row, col, True, True): + return -1 + + sparkline = {"locations": [xl_rowcol_to_cell(row, col)]} + + if options is None: + options = {} + + # Valid input parameters. + valid_parameters = { + "location", + "range", + "type", + "high_point", + "low_point", + "negative_points", + "first_point", + "last_point", + "markers", + "style", + "series_color", + "negative_color", + "markers_color", + "first_color", + "last_color", + "high_color", + "low_color", + "max", + "min", + "axis", + "reverse", + "empty_cells", + "show_hidden", + "plot_hidden", + "date_axis", + "weight", + } + + # Check for valid input parameters. + for param_key in options.keys(): + if param_key not in valid_parameters: + warn(f"Unknown parameter '{param_key}' in add_sparkline()") + return -1 + + # 'range' is a required parameter. + if "range" not in options: + warn("Parameter 'range' is required in add_sparkline()") + return -2 + + # Handle the sparkline type. + spark_type = options.get("type", "line") + + if spark_type not in ("line", "column", "win_loss"): + warn( + "Parameter 'type' must be 'line', 'column' " + "or 'win_loss' in add_sparkline()" + ) + return -2 + + if spark_type == "win_loss": + spark_type = "stacked" + sparkline["type"] = spark_type + + # We handle single location/range values or list of values. + if "location" in options: + if isinstance(options["location"], list): + sparkline["locations"] = options["location"] + else: + sparkline["locations"] = [options["location"]] + + if isinstance(options["range"], list): + sparkline["ranges"] = options["range"] + else: + sparkline["ranges"] = [options["range"]] + + range_count = len(sparkline["ranges"]) + location_count = len(sparkline["locations"]) + + # The ranges and locations must match. + if range_count != location_count: + warn( + "Must have the same number of location and range " + "parameters in add_sparkline()" + ) + return -2 + + # Store the count. + sparkline["count"] = len(sparkline["locations"]) + + # Get the worksheet name for the range conversion below. + sheetname = quote_sheetname(self.name) + + # Cleanup the input ranges. + new_ranges = [] + for spark_range in sparkline["ranges"]: + # Remove the absolute reference $ symbols. + spark_range = spark_range.replace("$", "") + + # Remove the = from formula. + spark_range = spark_range.lstrip("=") + + # Convert a simple range into a full Sheet1!A1:D1 range. + if "!" not in spark_range: + spark_range = sheetname + "!" + spark_range + + new_ranges.append(spark_range) + + sparkline["ranges"] = new_ranges + + # Cleanup the input locations. + new_locations = [] + for location in sparkline["locations"]: + location = location.replace("$", "") + new_locations.append(location) + + sparkline["locations"] = new_locations + + # Map options. + sparkline["high"] = options.get("high_point") + sparkline["low"] = options.get("low_point") + sparkline["negative"] = options.get("negative_points") + sparkline["first"] = options.get("first_point") + sparkline["last"] = options.get("last_point") + sparkline["markers"] = options.get("markers") + sparkline["min"] = options.get("min") + sparkline["max"] = options.get("max") + sparkline["axis"] = options.get("axis") + sparkline["reverse"] = options.get("reverse") + sparkline["hidden"] = options.get("show_hidden") + sparkline["weight"] = options.get("weight") + + # Map empty cells options. + empty = options.get("empty_cells", "") + + if empty == "zero": + sparkline["empty"] = 0 + elif empty == "connect": + sparkline["empty"] = "span" + else: + sparkline["empty"] = "gap" + + # Map the date axis range. + date_range = options.get("date_axis") + + if date_range and "!" not in date_range: + date_range = sheetname + "!" + date_range + + sparkline["date_axis"] = date_range + + # Set the sparkline styles. + style_id = options.get("style", 0) + style = _get_sparkline_style(style_id) + + sparkline["series_color"] = style["series"] + sparkline["negative_color"] = style["negative"] + sparkline["markers_color"] = style["markers"] + sparkline["first_color"] = style["first"] + sparkline["last_color"] = style["last"] + sparkline["high_color"] = style["high"] + sparkline["low_color"] = style["low"] + + # Override the style colors with user defined colors. + self._set_spark_color(sparkline, options, "series_color") + self._set_spark_color(sparkline, options, "negative_color") + self._set_spark_color(sparkline, options, "markers_color") + self._set_spark_color(sparkline, options, "first_color") + self._set_spark_color(sparkline, options, "last_color") + self._set_spark_color(sparkline, options, "high_color") + self._set_spark_color(sparkline, options, "low_color") + + self.sparklines.append(sparkline) + + return 0 + + @convert_range_args + def set_selection(self, first_row, first_col, last_row, last_col): + """ + Set the selected cell or cells in a worksheet + + Args: + first_row: The first row of the cell range. (zero indexed). + first_col: The first column of the cell range. + last_row: The last row of the cell range. (zero indexed). + last_col: The last column of the cell range. + + Returns: + 0: Nothing. + """ + pane = None + + # Range selection. Do this before swapping max/min to allow the + # selection direction to be reversed. + active_cell = xl_rowcol_to_cell(first_row, first_col) + + # Swap last row/col for first row/col if necessary + if first_row > last_row: + (first_row, last_row) = (last_row, first_row) + + if first_col > last_col: + (first_col, last_col) = (last_col, first_col) + + sqref = xl_range(first_row, first_col, last_row, last_col) + + # Selection isn't set for cell A1. + if sqref == "A1": + return + + self.selections = [[pane, active_cell, sqref]] + + @convert_cell_args + def set_top_left_cell(self, row=0, col=0): + """ + Set the first visible cell at the top left of a worksheet. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + + Returns: + 0: Nothing. + """ + + if row == 0 and col == 0: + return + + self.top_left_cell = xl_rowcol_to_cell(row, col) + + def outline_settings( + self, visible=1, symbols_below=1, symbols_right=1, auto_style=0 + ): + """ + Control outline settings. + + Args: + visible: Outlines are visible. Optional, defaults to True. + symbols_below: Show row outline symbols below the outline bar. + Optional, defaults to True. + symbols_right: Show column outline symbols to the right of the + outline bar. Optional, defaults to True. + auto_style: Use Automatic style. Optional, defaults to False. + + Returns: + 0: Nothing. + """ + self.outline_on = visible + self.outline_below = symbols_below + self.outline_right = symbols_right + self.outline_style = auto_style + + self.outline_changed = True + + @convert_cell_args + def freeze_panes(self, row, col, top_row=None, left_col=None, pane_type=0): + """ + Create worksheet panes and mark them as frozen. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + top_row: Topmost visible row in scrolling region of pane. + left_col: Leftmost visible row in scrolling region of pane. + + Returns: + 0: Nothing. + + """ + if top_row is None: + top_row = row + + if left_col is None: + left_col = col + + self.panes = [row, col, top_row, left_col, pane_type] + + @convert_cell_args + def split_panes(self, x, y, top_row=None, left_col=None): + """ + Create worksheet panes and mark them as split. + + Args: + x: The position for the vertical split. + y: The position for the horizontal split. + top_row: Topmost visible row in scrolling region of pane. + left_col: Leftmost visible row in scrolling region of pane. + + Returns: + 0: Nothing. + + """ + # Same as freeze panes with a different pane type. + self.freeze_panes(x, y, top_row, left_col, 2) + + def set_zoom(self, zoom=100): + """ + Set the worksheet zoom factor. + + Args: + zoom: Scale factor: 10 <= zoom <= 400. + + Returns: + Nothing. + + """ + # Ensure the zoom scale is in Excel's range. + if zoom < 10 or zoom > 400: + warn(f"Zoom factor '{zoom}' outside range: 10 <= zoom <= 400") + zoom = 100 + + self.zoom = int(zoom) + + def right_to_left(self): + """ + Display the worksheet right to left for some versions of Excel. + + Args: + None. + + Returns: + Nothing. + + """ + self.is_right_to_left = 1 + + def hide_zero(self): + """ + Hide zero values in worksheet cells. + + Args: + None. + + Returns: + Nothing. + + """ + self.show_zeros = 0 + + def set_tab_color(self, color): + """ + Set the color of the worksheet tab. + + Args: + color: A #RGB color index. + + Returns: + Nothing. + + """ + self.tab_color = _xl_color(color) + + def protect(self, password="", options=None): + """ + Set the password and protection options of the worksheet. + + Args: + password: An optional password string. + options: A dictionary of worksheet objects to protect. + + Returns: + Nothing. + + """ + if password != "": + password = self._encode_password(password) + + if not options: + options = {} + + # Default values for objects that can be protected. + defaults = { + "sheet": True, + "content": False, + "objects": False, + "scenarios": False, + "format_cells": False, + "format_columns": False, + "format_rows": False, + "insert_columns": False, + "insert_rows": False, + "insert_hyperlinks": False, + "delete_columns": False, + "delete_rows": False, + "select_locked_cells": True, + "sort": False, + "autofilter": False, + "pivot_tables": False, + "select_unlocked_cells": True, + } + + # Overwrite the defaults with user specified values. + for key in options.keys(): + if key in defaults: + defaults[key] = options[key] + else: + warn(f"Unknown protection object: '{key}'") + + # Set the password after the user defined values. + defaults["password"] = password + + self.protect_options = defaults + + def unprotect_range(self, cell_range, range_name=None, password=None): + """ + Unprotect ranges within a protected worksheet. + + Args: + cell_range: The cell or cell range to unprotect. + range_name: An optional name for the range. + password: An optional password string. (undocumented) + + Returns: + 0: Success. + -1: Parameter error. + + """ + if cell_range is None: + warn("Cell range must be specified in unprotect_range()") + return -1 + + # Sanitize the cell range. + cell_range = cell_range.lstrip("=") + cell_range = cell_range.replace("$", "") + + self.num_protected_ranges += 1 + + if range_name is None: + range_name = "Range" + str(self.num_protected_ranges) + + if password: + password = self._encode_password(password) + + self.protected_ranges.append((cell_range, range_name, password)) + + return 0 + + @convert_cell_args + def insert_button(self, row, col, options=None): + """ + Insert a button form object into the worksheet. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + options: Button formatting options. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + + """ + # Check insert (row, col) without storing. + if self._check_dimensions(row, col, True, True): + warn(f"Cannot insert button at ({row}, {col}).") + return -1 + + if options is None: + options = {} + + button = self._button_params(row, col, options) + + self.buttons_list.append(button) + + self.has_vml = 1 + + return 0 + + @convert_cell_args + def insert_checkbox(self, row, col, boolean, cell_format=None): + """ + Insert a boolean checkbox in a worksheet cell. + + Args: + row: The cell row (zero indexed). + col: The cell column (zero indexed). + boolean: The boolean value to display as a checkbox. + cell_format: Cell Format object. (optional) + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + + """ + # Ensure that the checkbox property is set in the user defined format. + if cell_format and not cell_format.checkbox: + # This needs to be fixed with a clone. + cell_format.set_checkbox() + + # If no format is supplied create and/or use the default checkbox format. + if not cell_format: + if not self.default_checkbox_format: + self.default_checkbox_format = self.workbook_add_format() + self.default_checkbox_format.set_checkbox() + + cell_format = self.default_checkbox_format + + return self._write_boolean(row, col, boolean, cell_format) + + ########################################################################### + # + # Public API. Page Setup methods. + # + ########################################################################### + def set_landscape(self): + """ + Set the page orientation as landscape. + + Args: + None. + + Returns: + Nothing. + + """ + self.orientation = 0 + self.page_setup_changed = True + + def set_portrait(self): + """ + Set the page orientation as portrait. + + Args: + None. + + Returns: + Nothing. + + """ + self.orientation = 1 + self.page_setup_changed = True + + def set_page_view(self, view=1): + """ + Set the page view mode. + + Args: + 0: Normal view mode + 1: Page view mode (the default) + 2: Page break view mode + + Returns: + Nothing. + + """ + self.page_view = view + + def set_pagebreak_view(self): + """ + Set the page view mode. + + Args: + None. + + Returns: + Nothing. + + """ + self.page_view = 2 + + def set_paper(self, paper_size): + """ + Set the paper type. US Letter = 1, A4 = 9. + + Args: + paper_size: Paper index. + + Returns: + Nothing. + + """ + if paper_size: + self.paper_size = paper_size + self.page_setup_changed = True + + def center_horizontally(self): + """ + Center the page horizontally. + + Args: + None. + + Returns: + Nothing. + + """ + self.print_options_changed = True + self.hcenter = 1 + + def center_vertically(self): + """ + Center the page vertically. + + Args: + None. + + Returns: + Nothing. + + """ + self.print_options_changed = True + self.vcenter = 1 + + def set_margins(self, left=0.7, right=0.7, top=0.75, bottom=0.75): + """ + Set all the page margins in inches. + + Args: + left: Left margin. + right: Right margin. + top: Top margin. + bottom: Bottom margin. + + Returns: + Nothing. + + """ + self.margin_left = left + self.margin_right = right + self.margin_top = top + self.margin_bottom = bottom + + def set_header(self, header="", options=None, margin=None): + """ + Set the page header caption and optional margin. + + Args: + header: Header string. + margin: Header margin. + options: Header options, mainly for images. + + Returns: + Nothing. + + """ + header_orig = header + header = header.replace("&[Picture]", "&G") + + if len(header) > 255: + warn("Header string cannot be longer than Excel's limit of 255 characters") + return + + if options is not None: + # For backward compatibility allow options to be the margin. + if not isinstance(options, dict): + options = {"margin": options} + else: + options = {} + + # Copy the user defined options so they aren't modified. + options = options.copy() + + # For backward compatibility. + if margin is not None: + options["margin"] = margin + + # Reset the list in case the function is called more than once. + self.header_images = [] + + if options.get("image_left"): + self.header_images.append( + [options.get("image_left"), options.get("image_data_left"), "LH"] + ) + + if options.get("image_center"): + self.header_images.append( + [options.get("image_center"), options.get("image_data_center"), "CH"] + ) + + if options.get("image_right"): + self.header_images.append( + [options.get("image_right"), options.get("image_data_right"), "RH"] + ) + + placeholder_count = header.count("&G") + image_count = len(self.header_images) + + if placeholder_count != image_count: + warn( + f"Number of footer images '{image_count}' doesn't match placeholder " + f"count '{placeholder_count}' in string: {header_orig}" + ) + self.header_images = [] + return + + if "align_with_margins" in options: + self.header_footer_aligns = options["align_with_margins"] + + if "scale_with_doc" in options: + self.header_footer_scales = options["scale_with_doc"] + + self.header = header + self.margin_header = options.get("margin", 0.3) + self.header_footer_changed = True + + if image_count: + self.has_header_vml = True + + def set_footer(self, footer="", options=None, margin=None): + """ + Set the page footer caption and optional margin. + + Args: + footer: Footer string. + margin: Footer margin. + options: Footer options, mainly for images. + + Returns: + Nothing. + + """ + footer_orig = footer + footer = footer.replace("&[Picture]", "&G") + + if len(footer) > 255: + warn("Footer string cannot be longer than Excel's limit of 255 characters") + return + + if options is not None: + # For backward compatibility allow options to be the margin. + if not isinstance(options, dict): + options = {"margin": options} + else: + options = {} + + # Copy the user defined options so they aren't modified. + options = options.copy() + + # For backward compatibility. + if margin is not None: + options["margin"] = margin + + # Reset the list in case the function is called more than once. + self.footer_images = [] + + if options.get("image_left"): + self.footer_images.append( + [options.get("image_left"), options.get("image_data_left"), "LF"] + ) + + if options.get("image_center"): + self.footer_images.append( + [options.get("image_center"), options.get("image_data_center"), "CF"] + ) + + if options.get("image_right"): + self.footer_images.append( + [options.get("image_right"), options.get("image_data_right"), "RF"] + ) + + placeholder_count = footer.count("&G") + image_count = len(self.footer_images) + + if placeholder_count != image_count: + warn( + f"Number of footer images '{image_count}' doesn't match placeholder " + f"count '{placeholder_count}' in string: {footer_orig}" + ) + self.footer_images = [] + return + + if "align_with_margins" in options: + self.header_footer_aligns = options["align_with_margins"] + + if "scale_with_doc" in options: + self.header_footer_scales = options["scale_with_doc"] + + self.footer = footer + self.margin_footer = options.get("margin", 0.3) + self.header_footer_changed = True + + if image_count: + self.has_header_vml = True + + def repeat_rows(self, first_row, last_row=None): + """ + Set the rows to repeat at the top of each printed page. + + Args: + first_row: Start row for range. + last_row: End row for range. + + Returns: + Nothing. + + """ + if last_row is None: + last_row = first_row + + # Convert rows to 1 based. + first_row += 1 + last_row += 1 + + # Create the row range area like: $1:$2. + area = f"${first_row}:${last_row}" + + # Build up the print titles area "Sheet1!$1:$2" + sheetname = quote_sheetname(self.name) + self.repeat_row_range = sheetname + "!" + area + + @convert_column_args + def repeat_columns(self, first_col, last_col=None): + """ + Set the columns to repeat at the left hand side of each printed page. + + Args: + first_col: Start column for range. + last_col: End column for range. + + Returns: + Nothing. + + """ + if last_col is None: + last_col = first_col + + # Convert to A notation. + first_col = xl_col_to_name(first_col, 1) + last_col = xl_col_to_name(last_col, 1) + + # Create a column range like $C:$D. + area = first_col + ":" + last_col + + # Build up the print area range "=Sheet2!$C:$D" + sheetname = quote_sheetname(self.name) + self.repeat_col_range = sheetname + "!" + area + + def hide_gridlines(self, option=1): + """ + Set the option to hide gridlines on the screen and the printed page. + + Args: + option: 0 : Don't hide gridlines + 1 : Hide printed gridlines only + 2 : Hide screen and printed gridlines + + Returns: + Nothing. + + """ + if option == 0: + self.print_gridlines = 1 + self.screen_gridlines = 1 + self.print_options_changed = True + elif option == 1: + self.print_gridlines = 0 + self.screen_gridlines = 1 + else: + self.print_gridlines = 0 + self.screen_gridlines = 0 + + def print_row_col_headers(self): + """ + Set the option to print the row and column headers on the printed page. + + Args: + None. + + Returns: + Nothing. + + """ + self.print_headers = True + self.print_options_changed = True + + def hide_row_col_headers(self): + """ + Set the option to hide the row and column headers on the worksheet. + + Args: + None. + + Returns: + Nothing. + + """ + self.row_col_headers = True + + @convert_range_args + def print_area(self, first_row, first_col, last_row, last_col): + """ + Set the print area in the current worksheet. + + Args: + first_row: The first row of the cell range. (zero indexed). + first_col: The first column of the cell range. + last_row: The last row of the cell range. (zero indexed). + last_col: The last column of the cell range. + + Returns: + 0: Success. + -1: Row or column is out of worksheet bounds. + + """ + # Set the print area in the current worksheet. + + # Ignore max print area since it is the same as no area for Excel. + if ( + first_row == 0 + and first_col == 0 + and last_row == self.xls_rowmax - 1 + and last_col == self.xls_colmax - 1 + ): + return -1 + + # Build up the print area range "Sheet1!$A$1:$C$13". + area = self._convert_name_area(first_row, first_col, last_row, last_col) + self.print_area_range = area + + return 0 + + def print_across(self): + """ + Set the order in which pages are printed. + + Args: + None. + + Returns: + Nothing. + + """ + self.page_order = 1 + self.page_setup_changed = True + + def fit_to_pages(self, width, height): + """ + Fit the printed area to a specific number of pages both vertically and + horizontally. + + Args: + width: Number of pages horizontally. + height: Number of pages vertically. + + Returns: + Nothing. + + """ + self.fit_page = 1 + self.fit_width = width + self.fit_height = height + self.page_setup_changed = True + + def set_start_page(self, start_page): + """ + Set the start page number when printing. + + Args: + start_page: Start page number. + + Returns: + Nothing. + + """ + self.page_start = start_page + + def set_print_scale(self, scale): + """ + Set the scale factor for the printed page. + + Args: + scale: Print scale. 10 <= scale <= 400. + + Returns: + Nothing. + + """ + # Confine the scale to Excel's range. + if scale < 10 or scale > 400: + warn(f"Print scale '{scale}' outside range: 10 <= scale <= 400") + return + + # Turn off "fit to page" option when print scale is on. + self.fit_page = 0 + + self.print_scale = int(scale) + self.page_setup_changed = True + + def print_black_and_white(self): + """ + Set the option to print the worksheet in black and white. + + Args: + None. + + Returns: + Nothing. + + """ + self.black_white = True + self.page_setup_changed = True + + def set_h_pagebreaks(self, breaks): + """ + Set the horizontal page breaks on a worksheet. + + Args: + breaks: List of rows where the page breaks should be added. + + Returns: + Nothing. + + """ + self.hbreaks = breaks + + def set_v_pagebreaks(self, breaks): + """ + Set the horizontal page breaks on a worksheet. + + Args: + breaks: List of columns where the page breaks should be added. + + Returns: + Nothing. + + """ + self.vbreaks = breaks + + def set_vba_name(self, name=None): + """ + Set the VBA name for the worksheet. By default this is the + same as the sheet name: i.e., Sheet1 etc. + + Args: + name: The VBA name for the worksheet. + + Returns: + Nothing. + + """ + if name is not None: + self.vba_codename = name + else: + self.vba_codename = "Sheet" + str(self.index + 1) + + def ignore_errors(self, options=None): + """ + Ignore various Excel errors/warnings in a worksheet for user defined + ranges. + + Args: + options: A dict of ignore errors keys with cell range values. + + Returns: + 0: Success. + -1: Incorrect parameter or option. + + """ + if options is None: + return -1 + + # Copy the user defined options so they aren't modified. + options = options.copy() + + # Valid input parameters. + valid_parameters = { + "number_stored_as_text", + "eval_error", + "formula_differs", + "formula_range", + "formula_unlocked", + "empty_cell_reference", + "list_data_validation", + "calculated_column", + "two_digit_text_year", + } + + # Check for valid input parameters. + for param_key in options.keys(): + if param_key not in valid_parameters: + warn(f"Unknown parameter '{param_key}' in ignore_errors()") + return -1 + + self.ignored_errors = options + + return 0 + + ########################################################################### + # + # Private API. + # + ########################################################################### + def _initialize(self, init_data): + self.name = init_data["name"] + self.index = init_data["index"] + self.str_table = init_data["str_table"] + self.worksheet_meta = init_data["worksheet_meta"] + self.constant_memory = init_data["constant_memory"] + self.tmpdir = init_data["tmpdir"] + self.date_1904 = init_data["date_1904"] + self.strings_to_numbers = init_data["strings_to_numbers"] + self.strings_to_formulas = init_data["strings_to_formulas"] + self.strings_to_urls = init_data["strings_to_urls"] + self.nan_inf_to_errors = init_data["nan_inf_to_errors"] + self.default_date_format = init_data["default_date_format"] + self.default_url_format = init_data["default_url_format"] + self.workbook_add_format = init_data["workbook_add_format"] + self.excel2003_style = init_data["excel2003_style"] + self.remove_timezone = init_data["remove_timezone"] + self.max_url_length = init_data["max_url_length"] + self.use_future_functions = init_data["use_future_functions"] + self.embedded_images = init_data["embedded_images"] + + if self.excel2003_style: + self.original_row_height = 12.75 + self.default_row_height = 12.75 + self.default_row_pixels = 17 + self.margin_left = 0.75 + self.margin_right = 0.75 + self.margin_top = 1 + self.margin_bottom = 1 + self.margin_header = 0.5 + self.margin_footer = 0.5 + self.header_footer_aligns = False + + # Open a temp filehandle to store row data in constant_memory mode. + if self.constant_memory: + # This is sub-optimal but we need to create a temp file + # with utf8 encoding in Python < 3. + (fd, filename) = tempfile.mkstemp(dir=self.tmpdir) + os.close(fd) + self.row_data_filename = filename + # pylint: disable=consider-using-with + self.row_data_fh = open(filename, mode="w+", encoding="utf-8") + + # Set as the worksheet filehandle until the file is assembled. + self.fh = self.row_data_fh + + def _assemble_xml_file(self): + # Assemble and write the XML file. + + # Write the XML declaration. + self._xml_declaration() + + # Write the root worksheet element. + self._write_worksheet() + + # Write the worksheet properties. + self._write_sheet_pr() + + # Write the worksheet dimensions. + self._write_dimension() + + # Write the sheet view properties. + self._write_sheet_views() + + # Write the sheet format properties. + self._write_sheet_format_pr() + + # Write the sheet column info. + self._write_cols() + + # Write the worksheet data such as rows columns and cells. + if not self.constant_memory: + self._write_sheet_data() + else: + self._write_optimized_sheet_data() + + # Write the sheetProtection element. + self._write_sheet_protection() + + # Write the protectedRanges element. + self._write_protected_ranges() + + # Write the phoneticPr element. + if self.excel2003_style: + self._write_phonetic_pr() + + # Write the autoFilter element. + self._write_auto_filter() + + # Write the mergeCells element. + self._write_merge_cells() + + # Write the conditional formats. + self._write_conditional_formats() + + # Write the dataValidations element. + self._write_data_validations() + + # Write the hyperlink element. + self._write_hyperlinks() + + # Write the printOptions element. + self._write_print_options() + + # Write the worksheet page_margins. + self._write_page_margins() + + # Write the worksheet page setup. + self._write_page_setup() + + # Write the headerFooter element. + self._write_header_footer() + + # Write the rowBreaks element. + self._write_row_breaks() + + # Write the colBreaks element. + self._write_col_breaks() + + # Write the ignoredErrors element. + self._write_ignored_errors() + + # Write the drawing element. + self._write_drawings() + + # Write the legacyDrawing element. + self._write_legacy_drawing() + + # Write the legacyDrawingHF element. + self._write_legacy_drawing_hf() + + # Write the picture element, for the background. + self._write_picture() + + # Write the tableParts element. + self._write_table_parts() + + # Write the extLst elements. + self._write_ext_list() + + # Close the worksheet tag. + self._xml_end_tag("worksheet") + + # Close the file. + self._xml_close() + + def _check_dimensions(self, row, col, ignore_row=False, ignore_col=False): + # Check that row and col are valid and store the max and min + # values for use in other methods/elements. The ignore_row / + # ignore_col flags is used to indicate that we wish to perform + # the dimension check without storing the value. The ignore + # flags are use by set_row() and data_validate. + + # Check that the row/col are within the worksheet bounds. + if row < 0 or col < 0: + return -1 + if row >= self.xls_rowmax or col >= self.xls_colmax: + return -1 + + # In constant_memory mode we don't change dimensions for rows + # that are already written. + if not ignore_row and not ignore_col and self.constant_memory: + if row < self.previous_row: + return -2 + + if not ignore_row: + if self.dim_rowmin is None or row < self.dim_rowmin: + self.dim_rowmin = row + if self.dim_rowmax is None or row > self.dim_rowmax: + self.dim_rowmax = row + + if not ignore_col: + if self.dim_colmin is None or col < self.dim_colmin: + self.dim_colmin = col + if self.dim_colmax is None or col > self.dim_colmax: + self.dim_colmax = col + + return 0 + + def _convert_date_time(self, dt_obj): + # Convert a datetime object to an Excel serial date and time. + return _datetime_to_excel_datetime(dt_obj, self.date_1904, self.remove_timezone) + + def _convert_name_area(self, row_num_1, col_num_1, row_num_2, col_num_2): + # Convert zero indexed rows and columns to the format required by + # worksheet named ranges, eg, "Sheet1!$A$1:$C$13". + + range1 = "" + range2 = "" + area = "" + row_col_only = 0 + + # Convert to A1 notation. + col_char_1 = xl_col_to_name(col_num_1, 1) + col_char_2 = xl_col_to_name(col_num_2, 1) + row_char_1 = "$" + str(row_num_1 + 1) + row_char_2 = "$" + str(row_num_2 + 1) + + # We need to handle special cases that refer to rows or columns only. + if row_num_1 == 0 and row_num_2 == self.xls_rowmax - 1: + range1 = col_char_1 + range2 = col_char_2 + row_col_only = 1 + elif col_num_1 == 0 and col_num_2 == self.xls_colmax - 1: + range1 = row_char_1 + range2 = row_char_2 + row_col_only = 1 + else: + range1 = col_char_1 + row_char_1 + range2 = col_char_2 + row_char_2 + + # A repeated range is only written once (if it isn't a special case). + if range1 == range2 and not row_col_only: + area = range1 + else: + area = range1 + ":" + range2 + + # Build up the print area range "Sheet1!$A$1:$C$13". + sheetname = quote_sheetname(self.name) + area = sheetname + "!" + area + + return area + + def _sort_pagebreaks(self, breaks): + # This is an internal method used to filter elements of a list of + # pagebreaks used in the _store_hbreak() and _store_vbreak() methods. + # It: + # 1. Removes duplicate entries from the list. + # 2. Sorts the list. + # 3. Removes 0 from the list if present. + if not breaks: + return [] + + breaks_set = set(breaks) + + if 0 in breaks_set: + breaks_set.remove(0) + + breaks_list = list(breaks_set) + breaks_list.sort() + + # The Excel 2007 specification says that the maximum number of page + # breaks is 1026. However, in practice it is actually 1023. + max_num_breaks = 1023 + if len(breaks_list) > max_num_breaks: + breaks_list = breaks_list[:max_num_breaks] + + return breaks_list + + def _extract_filter_tokens(self, expression): + # Extract the tokens from the filter expression. The tokens are mainly + # non-whitespace groups. The only tricky part is to extract string + # tokens that contain whitespace and/or quoted double quotes (Excel's + # escaped quotes). + # + # Examples: 'x < 2000' + # 'x > 2000 and x < 5000' + # 'x = "foo"' + # 'x = "foo bar"' + # 'x = "foo "" bar"' + # + if not expression: + return [] + + token_re = re.compile(r'"(?:[^"]|"")*"|\S+') + tokens = token_re.findall(expression) + + new_tokens = [] + # Remove single leading and trailing quotes and un-escape other quotes. + for token in tokens: + if token.startswith('"'): + token = token[1:] + + if token.endswith('"'): + token = token[:-1] + + token = token.replace('""', '"') + + new_tokens.append(token) + + return new_tokens + + def _parse_filter_expression(self, expression, tokens): + # Converts the tokens of a possibly conditional expression into 1 or 2 + # sub expressions for further parsing. + # + # Examples: + # ('x', '==', 2000) -> exp1 + # ('x', '>', 2000, 'and', 'x', '<', 5000) -> exp1 and exp2 + + if len(tokens) == 7: + # The number of tokens will be either 3 (for 1 expression) + # or 7 (for 2 expressions). + conditional = tokens[3] + + if re.match("(and|&&)", conditional): + conditional = 0 + elif re.match(r"(or|\|\|)", conditional): + conditional = 1 + else: + warn( + f"Token '{conditional}' is not a valid conditional " + f"in filter expression '{expression}'" + ) + + expression_1 = self._parse_filter_tokens(expression, tokens[0:3]) + expression_2 = self._parse_filter_tokens(expression, tokens[4:7]) + return expression_1 + [conditional] + expression_2 + + return self._parse_filter_tokens(expression, tokens) + + def _parse_filter_tokens(self, expression, tokens): + # Parse the 3 tokens of a filter expression and return the operator + # and token. The use of numbers instead of operators is a legacy of + # Spreadsheet::WriteExcel. + operators = { + "==": 2, + "=": 2, + "=~": 2, + "eq": 2, + "!=": 5, + "!~": 5, + "ne": 5, + "<>": 5, + "<": 1, + "<=": 3, + ">": 4, + ">=": 6, + } + + operator = operators.get(tokens[1], None) + token = tokens[2] + + # Special handling of "Top" filter expressions. + if re.match("top|bottom", tokens[0].lower()): + value = int(tokens[1]) + + if value < 1 or value > 500: + warn( + f"The value '{token}' in expression '{expression}' " + f"must be in the range 1 to 500" + ) + + token = token.lower() + + if token not in ("items", "%"): + warn( + f"The type '{token}' in expression '{expression}' " + f"must be either 'items' or '%%'" + ) + + if tokens[0].lower() == "top": + operator = 30 + else: + operator = 32 + + if tokens[2] == "%": + operator += 1 + + token = str(value) + + if not operator and tokens[0]: + warn( + f"Token '{token[0]}' is not a valid operator " + f"in filter expression '{expression}'." + ) + + # Special handling for Blanks/NonBlanks. + if re.match("blanks|nonblanks", token.lower()): + # Only allow Equals or NotEqual in this context. + if operator not in (2, 5): + warn( + f"The operator '{tokens[1]}' in expression '{expression}' " + f"is not valid in relation to Blanks/NonBlanks'." + ) + + token = token.lower() + + # The operator should always be 2 (=) to flag a "simple" equality + # in the binary record. Therefore we convert <> to =. + if token == "blanks": + if operator == 5: + token = " " + else: + if operator == 5: + operator = 2 + token = "blanks" + else: + operator = 5 + token = " " + + # if the string token contains an Excel match character then change the + # operator type to indicate a non "simple" equality. + if operator == 2 and re.search("[*?]", token): + operator = 22 + + return [operator, token] + + def _encode_password(self, password): + # Hash a worksheet password. Based on the algorithm in + # ECMA-376-4:2016, Office Open XML File Formats — Transitional + # Migration Features, Additional attributes for workbookProtection + # element (Part 1, §18.2.29). + digest = 0x0000 + + for char in password[::-1]: + digest = ((digest >> 14) & 0x01) | ((digest << 1) & 0x7FFF) + digest ^= ord(char) + + digest = ((digest >> 14) & 0x01) | ((digest << 1) & 0x7FFF) + digest ^= len(password) + digest ^= 0xCE4B + + return f"{digest:X}" + + def _prepare_image( + self, + index, + image_id, + drawing_id, + width, + height, + name, + image_type, + x_dpi, + y_dpi, + digest, + ): + # Set up images/drawings. + drawing_type = 2 + ( + row, + col, + _, + x_offset, + y_offset, + x_scale, + y_scale, + url, + tip, + anchor, + _, + description, + decorative, + ) = self.images[index] + + width *= x_scale + height *= y_scale + + # Scale by non 96dpi resolutions. + width *= 96.0 / x_dpi + height *= 96.0 / y_dpi + + dimensions = self._position_object_emus( + col, row, x_offset, y_offset, width, height, anchor + ) + # Convert from pixels to emus. + width = int(0.5 + (width * 9525)) + height = int(0.5 + (height * 9525)) + + # Create a Drawing obj to use with worksheet unless one already exists. + if not self.drawing: + drawing = Drawing() + drawing.embedded = 1 + self.drawing = drawing + + self.external_drawing_links.append( + ["/drawing", "../drawings/drawing" + str(drawing_id) + ".xml", None] + ) + else: + drawing = self.drawing + + drawing_object = drawing._add_drawing_object() + drawing_object["type"] = drawing_type + drawing_object["dimensions"] = dimensions + drawing_object["width"] = width + drawing_object["height"] = height + drawing_object["description"] = name + drawing_object["shape"] = None + drawing_object["anchor"] = anchor + drawing_object["rel_index"] = 0 + drawing_object["url_rel_index"] = 0 + drawing_object["tip"] = tip + drawing_object["decorative"] = decorative + + if description is not None: + drawing_object["description"] = description + + if url: + target = None + rel_type = "/hyperlink" + target_mode = "External" + + if re.match("(ftp|http)s?://", url): + target = self._escape_url(url) + + if re.match("^mailto:", url): + target = self._escape_url(url) + + if re.match("external:", url): + target = url.replace("external:", "") + target = self._escape_url(target) + # Additional escape not required in worksheet hyperlinks. + target = target.replace("#", "%23") + + if re.match(r"\w:", target) or re.match(r"\\", target): + target = "file:///" + target + else: + target = target.replace("\\", "/") + + if re.match("internal:", url): + target = url.replace("internal:", "#") + target_mode = None + + if target is not None: + if len(target) > self.max_url_length: + warn( + f"Ignoring URL '{url}' with link and/or anchor > " + f"{self.max_url_length} characters since it exceeds " + f"Excel's limit for URLs." + ) + else: + if not self.drawing_rels.get(url): + self.drawing_links.append([rel_type, target, target_mode]) + + drawing_object["url_rel_index"] = self._get_drawing_rel_index(url) + + if not self.drawing_rels.get(digest): + self.drawing_links.append( + ["/image", "../media/image" + str(image_id) + "." + image_type] + ) + + drawing_object["rel_index"] = self._get_drawing_rel_index(digest) + + def _prepare_shape(self, index, drawing_id): + # Set up shapes/drawings. + drawing_type = 3 + + ( + row, + col, + x_offset, + y_offset, + x_scale, + y_scale, + text, + anchor, + options, + description, + decorative, + ) = self.shapes[index] + + width = options.get("width", self.default_col_pixels * 3) + height = options.get("height", self.default_row_pixels * 6) + + width *= x_scale + height *= y_scale + + dimensions = self._position_object_emus( + col, row, x_offset, y_offset, width, height, anchor + ) + + # Convert from pixels to emus. + width = int(0.5 + (width * 9525)) + height = int(0.5 + (height * 9525)) + + # Create a Drawing obj to use with worksheet unless one already exists. + if not self.drawing: + drawing = Drawing() + drawing.embedded = 1 + self.drawing = drawing + + self.external_drawing_links.append( + ["/drawing", "../drawings/drawing" + str(drawing_id) + ".xml", None] + ) + else: + drawing = self.drawing + + shape = Shape("rect", "TextBox", options) + shape.text = text + + drawing_object = drawing._add_drawing_object() + drawing_object["type"] = drawing_type + drawing_object["dimensions"] = dimensions + drawing_object["width"] = width + drawing_object["height"] = height + drawing_object["description"] = description + drawing_object["shape"] = shape + drawing_object["anchor"] = anchor + drawing_object["rel_index"] = 0 + drawing_object["url_rel_index"] = 0 + drawing_object["tip"] = options.get("tip") + drawing_object["decorative"] = decorative + + url = options.get("url", None) + if url: + target = None + rel_type = "/hyperlink" + target_mode = "External" + + if re.match("(ftp|http)s?://", url): + target = self._escape_url(url) + + if re.match("^mailto:", url): + target = self._escape_url(url) + + if re.match("external:", url): + target = url.replace("external:", "file:///") + target = self._escape_url(target) + # Additional escape not required in worksheet hyperlinks. + target = target.replace("#", "%23") + + if re.match("internal:", url): + target = url.replace("internal:", "#") + target_mode = None + + if target is not None: + if len(target) > self.max_url_length: + warn( + f"Ignoring URL '{url}' with link and/or anchor > " + f"{self.max_url_length} characters since it exceeds " + f"Excel's limit for URLs." + ) + else: + if not self.drawing_rels.get(url): + self.drawing_links.append([rel_type, target, target_mode]) + + drawing_object["url_rel_index"] = self._get_drawing_rel_index(url) + + def _prepare_header_image( + self, image_id, width, height, name, image_type, position, x_dpi, y_dpi, digest + ): + # Set up an image without a drawing object for header/footer images. + + # Strip the extension from the filename. + name = re.sub(r"\..*$", "", name) + + if not self.vml_drawing_rels.get(digest): + self.vml_drawing_links.append( + ["/image", "../media/image" + str(image_id) + "." + image_type] + ) + + ref_id = self._get_vml_drawing_rel_index(digest) + + self.header_images_list.append( + [width, height, name, position, x_dpi, y_dpi, ref_id] + ) + + def _prepare_background(self, image_id, image_type): + # Set up an image without a drawing object for backgrounds. + self.external_background_links.append( + ["/image", "../media/image" + str(image_id) + "." + image_type] + ) + + def _prepare_chart(self, index, chart_id, drawing_id): + # Set up chart/drawings. + drawing_type = 1 + + ( + row, + col, + chart, + x_offset, + y_offset, + x_scale, + y_scale, + anchor, + description, + decorative, + ) = self.charts[index] + + chart.id = chart_id - 1 + + # Use user specified dimensions, if any. + width = int(0.5 + (chart.width * x_scale)) + height = int(0.5 + (chart.height * y_scale)) + + dimensions = self._position_object_emus( + col, row, x_offset, y_offset, width, height, anchor + ) + + # Set the chart name for the embedded object if it has been specified. + name = chart.chart_name + + # Create a Drawing obj to use with worksheet unless one already exists. + if not self.drawing: + drawing = Drawing() + drawing.embedded = 1 + self.drawing = drawing + + self.external_drawing_links.append( + ["/drawing", "../drawings/drawing" + str(drawing_id) + ".xml"] + ) + else: + drawing = self.drawing + + drawing_object = drawing._add_drawing_object() + drawing_object["type"] = drawing_type + drawing_object["dimensions"] = dimensions + drawing_object["width"] = width + drawing_object["height"] = height + drawing_object["name"] = name + drawing_object["shape"] = None + drawing_object["anchor"] = anchor + drawing_object["rel_index"] = self._get_drawing_rel_index() + drawing_object["url_rel_index"] = 0 + drawing_object["tip"] = None + drawing_object["description"] = description + drawing_object["decorative"] = decorative + + self.drawing_links.append( + ["/chart", "../charts/chart" + str(chart_id) + ".xml"] + ) + + def _position_object_emus( + self, col_start, row_start, x1, y1, width, height, anchor + ): + # Calculate the vertices that define the position of a graphical + # object within the worksheet in EMUs. + # + # The vertices are expressed as English Metric Units (EMUs). There are + # 12,700 EMUs per point. Therefore, 12,700 * 3 /4 = 9,525 EMUs per + # pixel + ( + col_start, + row_start, + x1, + y1, + col_end, + row_end, + x2, + y2, + x_abs, + y_abs, + ) = self._position_object_pixels( + col_start, row_start, x1, y1, width, height, anchor + ) + + # Convert the pixel values to EMUs. See above. + x1 = int(0.5 + 9525 * x1) + y1 = int(0.5 + 9525 * y1) + x2 = int(0.5 + 9525 * x2) + y2 = int(0.5 + 9525 * y2) + x_abs = int(0.5 + 9525 * x_abs) + y_abs = int(0.5 + 9525 * y_abs) + + return (col_start, row_start, x1, y1, col_end, row_end, x2, y2, x_abs, y_abs) + + # Calculate the vertices that define the position of a graphical object + # within the worksheet in pixels. + # + # +------------+------------+ + # | A | B | + # +-----+------------+------------+ + # | |(x1,y1) | | + # | 1 |(A1)._______|______ | + # | | | | | + # | | | | | + # +-----+----| OBJECT |-----+ + # | | | | | + # | 2 | |______________. | + # | | | (B2)| + # | | | (x2,y2)| + # +---- +------------+------------+ + # + # Example of an object that covers some of the area from cell A1 to B2. + # + # Based on the width and height of the object we need to calculate 8 vars: + # + # col_start, row_start, col_end, row_end, x1, y1, x2, y2. + # + # We also calculate the absolute x and y position of the top left vertex of + # the object. This is required for images. + # + # The width and height of the cells that the object occupies can be + # variable and have to be taken into account. + # + # The values of col_start and row_start are passed in from the calling + # function. The values of col_end and row_end are calculated by + # subtracting the width and height of the object from the width and + # height of the underlying cells. + # + def _position_object_pixels( + self, col_start, row_start, x1, y1, width, height, anchor + ): + # col_start # Col containing upper left corner of object. + # x1 # Distance to left side of object. + # + # row_start # Row containing top left corner of object. + # y1 # Distance to top of object. + # + # col_end # Col containing lower right corner of object. + # x2 # Distance to right side of object. + # + # row_end # Row containing bottom right corner of object. + # y2 # Distance to bottom of object. + # + # width # Width of object frame. + # height # Height of object frame. + # + # x_abs # Absolute distance to left side of object. + # y_abs # Absolute distance to top side of object. + x_abs = 0 + y_abs = 0 + + # Adjust start column for negative offsets. + # pylint: disable=chained-comparison + while x1 < 0 and col_start > 0: + x1 += self._size_col(col_start - 1) + col_start -= 1 + + # Adjust start row for negative offsets. + while y1 < 0 and row_start > 0: + y1 += self._size_row(row_start - 1) + row_start -= 1 + + # Ensure that the image isn't shifted off the page at top left. + x1 = max(0, x1) + y1 = max(0, y1) + + # Calculate the absolute x offset of the top-left vertex. + if self.col_size_changed: + for col_id in range(col_start): + x_abs += self._size_col(col_id) + else: + # Optimization for when the column widths haven't changed. + x_abs += self.default_col_pixels * col_start + + x_abs += x1 + + # Calculate the absolute y offset of the top-left vertex. + if self.row_size_changed: + for row_id in range(row_start): + y_abs += self._size_row(row_id) + else: + # Optimization for when the row heights haven't changed. + y_abs += self.default_row_pixels * row_start + + y_abs += y1 + + # Adjust start column for offsets that are greater than the col width. + while x1 >= self._size_col(col_start, anchor): + x1 -= self._size_col(col_start) + col_start += 1 + + # Adjust start row for offsets that are greater than the row height. + while y1 >= self._size_row(row_start, anchor): + y1 -= self._size_row(row_start) + row_start += 1 + + # Initialize end cell to the same as the start cell. + col_end = col_start + row_end = row_start + + # Don't offset the image in the cell if the row/col is hidden. + if self._size_col(col_start, anchor) > 0: + width = width + x1 + if self._size_row(row_start, anchor) > 0: + height = height + y1 + + # Subtract the underlying cell widths to find end cell of the object. + while width >= self._size_col(col_end, anchor): + width -= self._size_col(col_end, anchor) + col_end += 1 + + # Subtract the underlying cell heights to find end cell of the object. + while height >= self._size_row(row_end, anchor): + height -= self._size_row(row_end, anchor) + row_end += 1 + + # The end vertices are whatever is left from the width and height. + x2 = width + y2 = height + + return [col_start, row_start, x1, y1, col_end, row_end, x2, y2, x_abs, y_abs] + + def _size_col(self, col, anchor=0): + # Convert the width of a cell from character units to pixels. Excel + # rounds the column width to the nearest pixel. If the width hasn't + # been set by the user we use the default value. A hidden column is + # treated as having a width of zero unless it has the special + # "object_position" of 4 (size with cells). + max_digit_width = 7 # For Calibri 11. + padding = 5 + pixels = 0 + + # Look up the cell value to see if it has been changed. + if col in self.col_info: + width = self.col_info[col][0] + hidden = self.col_info[col][2] + + if width is None: + width = self.default_col_width + + # Convert to pixels. + if hidden and anchor != 4: + pixels = 0 + elif width < 1: + pixels = int(width * (max_digit_width + padding) + 0.5) + else: + pixels = int(width * max_digit_width + 0.5) + padding + else: + pixels = self.default_col_pixels + + return pixels + + def _size_row(self, row, anchor=0): + # Convert the height of a cell from character units to pixels. If the + # height hasn't been set by the user we use the default value. A + # hidden row is treated as having a height of zero unless it has the + # special "object_position" of 4 (size with cells). + pixels = 0 + + # Look up the cell value to see if it has been changed + if row in self.row_sizes: + height = self.row_sizes[row][0] + hidden = self.row_sizes[row][1] + + if hidden and anchor != 4: + pixels = 0 + else: + pixels = int(4.0 / 3.0 * height) + else: + pixels = int(4.0 / 3.0 * self.default_row_height) + + return pixels + + def _pixels_to_width(self, pixels): + # Convert the width of a cell from pixels to character units. + max_digit_width = 7.0 # For Calabri 11. + padding = 5.0 + + if pixels <= 12: + width = pixels / (max_digit_width + padding) + else: + width = (pixels - padding) / max_digit_width + + return width + + def _pixels_to_height(self, pixels): + # Convert the height of a cell from pixels to character units. + return 0.75 * pixels + + def _comment_params(self, row, col, string, options): + # This method handles the additional optional parameters to + # write_comment() as well as calculating the comment object + # position and vertices. + default_width = 128 + default_height = 74 + anchor = 0 + + params = { + "author": None, + "color": "#ffffe1", + "start_cell": None, + "start_col": None, + "start_row": None, + "visible": None, + "width": default_width, + "height": default_height, + "x_offset": None, + "x_scale": 1, + "y_offset": None, + "y_scale": 1, + "font_name": "Tahoma", + "font_size": 8, + "font_family": 2, + } + + # Overwrite the defaults with any user supplied values. Incorrect or + # misspelled parameters are silently ignored. + for key in options.keys(): + params[key] = options[key] + + # Ensure that a width and height have been set. + if not params["width"]: + params["width"] = default_width + if not params["height"]: + params["height"] = default_height + + # Set the comment background color. + params["color"] = _xl_color(params["color"]).lower() + + # Convert from Excel XML style color to XML html style color. + params["color"] = params["color"].replace("ff", "#", 1) + + # Convert a cell reference to a row and column. + if params["start_cell"] is not None: + (start_row, start_col) = xl_cell_to_rowcol(params["start_cell"]) + params["start_row"] = start_row + params["start_col"] = start_col + + # Set the default start cell and offsets for the comment. These are + # generally fixed in relation to the parent cell. However there are + # some edge cases for cells at the, er, edges. + row_max = self.xls_rowmax + col_max = self.xls_colmax + + if params["start_row"] is None: + if row == 0: + params["start_row"] = 0 + elif row == row_max - 3: + params["start_row"] = row_max - 7 + elif row == row_max - 2: + params["start_row"] = row_max - 6 + elif row == row_max - 1: + params["start_row"] = row_max - 5 + else: + params["start_row"] = row - 1 + + if params["y_offset"] is None: + if row == 0: + params["y_offset"] = 2 + elif row == row_max - 3: + params["y_offset"] = 16 + elif row == row_max - 2: + params["y_offset"] = 16 + elif row == row_max - 1: + params["y_offset"] = 14 + else: + params["y_offset"] = 10 + + if params["start_col"] is None: + if col == col_max - 3: + params["start_col"] = col_max - 6 + elif col == col_max - 2: + params["start_col"] = col_max - 5 + elif col == col_max - 1: + params["start_col"] = col_max - 4 + else: + params["start_col"] = col + 1 + + if params["x_offset"] is None: + if col == col_max - 3: + params["x_offset"] = 49 + elif col == col_max - 2: + params["x_offset"] = 49 + elif col == col_max - 1: + params["x_offset"] = 49 + else: + params["x_offset"] = 15 + + # Scale the size of the comment box if required. + if params["x_scale"]: + params["width"] = params["width"] * params["x_scale"] + + if params["y_scale"]: + params["height"] = params["height"] * params["y_scale"] + + # Round the dimensions to the nearest pixel. + params["width"] = int(0.5 + params["width"]) + params["height"] = int(0.5 + params["height"]) + + # Calculate the positions of the comment object. + vertices = self._position_object_pixels( + params["start_col"], + params["start_row"], + params["x_offset"], + params["y_offset"], + params["width"], + params["height"], + anchor, + ) + + # Add the width and height for VML. + vertices.append(params["width"]) + vertices.append(params["height"]) + + return [ + row, + col, + string, + params["author"], + params["visible"], + params["color"], + params["font_name"], + params["font_size"], + params["font_family"], + ] + [vertices] + + def _button_params(self, row, col, options): + # This method handles the parameters passed to insert_button() as well + # as calculating the button object position and vertices. + + default_height = self.default_row_pixels + default_width = self.default_col_pixels + anchor = 0 + + button_number = 1 + len(self.buttons_list) + button = {"row": row, "col": col, "font": {}} + params = {} + + # Overwrite the defaults with any user supplied values. Incorrect or + # misspelled parameters are silently ignored. + for key in options.keys(): + params[key] = options[key] + + # Set the button caption. + caption = params.get("caption") + + # Set a default caption if none was specified by user. + if caption is None: + caption = f"Button {button_number}" + + button["font"]["caption"] = caption + + # Set the macro name. + if params.get("macro"): + button["macro"] = "[0]!" + params["macro"] + else: + button["macro"] = f"[0]!Button{button_number}_Click" + + # Set the alt text for the button. + button["description"] = params.get("description") + + # Ensure that a width and height have been set. + params["width"] = params.get("width", default_width) + params["height"] = params.get("height", default_height) + + # Set the x/y offsets. + params["x_offset"] = params.get("x_offset", 0) + params["y_offset"] = params.get("y_offset", 0) + + # Scale the size of the button if required. + params["width"] = params["width"] * params.get("x_scale", 1) + params["height"] = params["height"] * params.get("y_scale", 1) + + # Round the dimensions to the nearest pixel. + params["width"] = int(0.5 + params["width"]) + params["height"] = int(0.5 + params["height"]) + + params["start_row"] = row + params["start_col"] = col + + # Calculate the positions of the button object. + vertices = self._position_object_pixels( + params["start_col"], + params["start_row"], + params["x_offset"], + params["y_offset"], + params["width"], + params["height"], + anchor, + ) + + # Add the width and height for VML. + vertices.append(params["width"]) + vertices.append(params["height"]) + + button["vertices"] = vertices + + return button + + def _prepare_vml_objects( + self, vml_data_id, vml_shape_id, vml_drawing_id, comment_id + ): + comments = [] + # Sort the comments into row/column order for easier comparison + # testing and set the external links for comments and buttons. + row_nums = sorted(self.comments.keys()) + + for row in row_nums: + col_nums = sorted(self.comments[row].keys()) + + for col in col_nums: + user_options = self.comments[row][col] + params = self._comment_params(*user_options) + self.comments[row][col] = params + + # Set comment visibility if required and not user defined. + if self.comments_visible: + if self.comments[row][col][4] is None: + self.comments[row][col][4] = 1 + + # Set comment author if not already user defined. + if self.comments[row][col][3] is None: + self.comments[row][col][3] = self.comments_author + + comments.append(self.comments[row][col]) + + self.external_vml_links.append( + ["/vmlDrawing", "../drawings/vmlDrawing" + str(vml_drawing_id) + ".vml"] + ) + + if self.has_comments: + self.comments_list = comments + + self.external_comment_links.append( + ["/comments", "../comments" + str(comment_id) + ".xml"] + ) + + count = len(comments) + start_data_id = vml_data_id + + # The VML o:idmap data id contains a comma separated range when there + # is more than one 1024 block of comments, like this: data="1,2". + for i in range(int(count / 1024)): + data_id = start_data_id + i + 1 + vml_data_id = f"{vml_data_id},{data_id}" + + self.vml_data_id = vml_data_id + self.vml_shape_id = vml_shape_id + + return count + + def _prepare_header_vml_objects(self, vml_header_id, vml_drawing_id): + # Set up external linkage for VML header/footer images. + + self.vml_header_id = vml_header_id + + self.external_vml_links.append( + ["/vmlDrawing", "../drawings/vmlDrawing" + str(vml_drawing_id) + ".vml"] + ) + + def _prepare_tables(self, table_id, seen): + # Set the table ids for the worksheet tables. + for table in self.tables: + table["id"] = table_id + + if table.get("name") is None: + # Set a default name. + table["name"] = "Table" + str(table_id) + + # Check for duplicate table names. + name = table["name"].lower() + + if name in seen: + raise DuplicateTableName( + f"Duplicate name '{table['name']}' used in worksheet.add_table()." + ) + + seen[name] = True + + # Store the link used for the rels file. + self.external_table_links.append( + ["/table", "../tables/table" + str(table_id) + ".xml"] + ) + table_id += 1 + + def _table_function_to_formula(self, function, col_name): + # Convert a table total function to a worksheet formula. + formula = "" + + # Escape special characters, as required by Excel. + col_name = col_name.replace("'", "''") + col_name = col_name.replace("#", "'#") + col_name = col_name.replace("]", "']") + col_name = col_name.replace("[", "'[") + + subtotals = { + "average": 101, + "countNums": 102, + "count": 103, + "max": 104, + "min": 105, + "stdDev": 107, + "sum": 109, + "var": 110, + } + + if function in subtotals: + func_num = subtotals[function] + formula = f"SUBTOTAL({func_num},[{col_name}])" + else: + warn(f"Unsupported function '{function}' in add_table()") + + return formula + + def _set_spark_color(self, sparkline, options, user_color): + # Set the sparkline color. + if user_color not in options: + return + + sparkline[user_color] = {"rgb": _xl_color(options[user_color])} + + def _get_range_data(self, row_start, col_start, row_end, col_end): + # Returns a range of data from the worksheet _table to be used in + # chart cached data. Strings are returned as SST ids and decoded + # in the workbook. Return None for data that doesn't exist since + # Excel can chart series with data missing. + + if self.constant_memory: + return () + + data = [] + + # Iterate through the table data. + for row_num in range(row_start, row_end + 1): + # Store None if row doesn't exist. + if row_num not in self.table: + data.append(None) + continue + + for col_num in range(col_start, col_end + 1): + if col_num in self.table[row_num]: + cell = self.table[row_num][col_num] + + cell_type = cell.__class__.__name__ + + if cell_type in ("Number", "Datetime"): + # Return a number with Excel's precision. + data.append(f"{cell.number:.16g}") + + elif cell_type == "String": + # Return a string from it's shared string index. + index = cell.string + string = self.str_table._get_shared_string(index) + + data.append(string) + + elif cell_type in ("Formula", "ArrayFormula"): + # Return the formula value. + value = cell.value + + if value is None: + value = 0 + + data.append(value) + + elif cell_type == "Blank": + # Return a empty cell. + data.append("") + else: + # Store None if column doesn't exist. + data.append(None) + + return data + + def _csv_join(self, *items): + # Create a csv string for use with data validation formulas and lists. + + # Convert non string types to string. + items = [str(item) if not isinstance(item, str) else item for item in items] + + return ",".join(items) + + def _escape_url(self, url): + # Don't escape URL if it looks already escaped. + if re.search("%[0-9a-fA-F]{2}", url): + return url + + # Can't use url.quote() here because it doesn't match Excel. + url = url.replace("%", "%25") + url = url.replace('"', "%22") + url = url.replace(" ", "%20") + url = url.replace("<", "%3c") + url = url.replace(">", "%3e") + url = url.replace("[", "%5b") + url = url.replace("]", "%5d") + url = url.replace("^", "%5e") + url = url.replace("`", "%60") + url = url.replace("{", "%7b") + url = url.replace("}", "%7d") + + return url + + def _get_drawing_rel_index(self, target=None): + # Get the index used to address a drawing rel link. + if target is None: + self.drawing_rels_id += 1 + return self.drawing_rels_id + + if self.drawing_rels.get(target): + return self.drawing_rels[target] + + self.drawing_rels_id += 1 + self.drawing_rels[target] = self.drawing_rels_id + return self.drawing_rels_id + + def _get_vml_drawing_rel_index(self, target=None): + # Get the index used to address a vml drawing rel link. + if self.vml_drawing_rels.get(target): + return self.vml_drawing_rels[target] + + self.vml_drawing_rels_id += 1 + self.vml_drawing_rels[target] = self.vml_drawing_rels_id + return self.vml_drawing_rels_id + + ########################################################################### + # + # The following font methods are, more or less, duplicated from the + # Styles class. Not the cleanest version of reuse but works for now. + # + ########################################################################### + def _write_font(self, xf_format): + # Write the <font> element. + xml_writer = self.rstring + + xml_writer._xml_start_tag("rPr") + + # Handle the main font properties. + if xf_format.bold: + xml_writer._xml_empty_tag("b") + if xf_format.italic: + xml_writer._xml_empty_tag("i") + if xf_format.font_strikeout: + xml_writer._xml_empty_tag("strike") + if xf_format.font_outline: + xml_writer._xml_empty_tag("outline") + if xf_format.font_shadow: + xml_writer._xml_empty_tag("shadow") + + # Handle the underline variants. + if xf_format.underline: + self._write_underline(xf_format.underline) + + # Handle super/subscript. + if xf_format.font_script == 1: + self._write_vert_align("superscript") + if xf_format.font_script == 2: + self._write_vert_align("subscript") + + # Write the font size + xml_writer._xml_empty_tag("sz", [("val", xf_format.font_size)]) + + # Handle colors. + if xf_format.theme == -1: + # Ignore for excel2003_style. + pass + elif xf_format.theme: + self._write_color("theme", xf_format.theme) + elif xf_format.color_indexed: + self._write_color("indexed", xf_format.color_indexed) + elif xf_format.font_color: + color = self._get_palette_color(xf_format.font_color) + self._write_rstring_color("rgb", color) + else: + self._write_rstring_color("theme", 1) + + # Write some other font properties related to font families. + xml_writer._xml_empty_tag("rFont", [("val", xf_format.font_name)]) + xml_writer._xml_empty_tag("family", [("val", xf_format.font_family)]) + + if xf_format.font_name == "Calibri" and not xf_format.hyperlink: + xml_writer._xml_empty_tag("scheme", [("val", xf_format.font_scheme)]) + + xml_writer._xml_end_tag("rPr") + + def _write_underline(self, underline): + # Write the underline font element. + attributes = [] + + # Handle the underline variants. + if underline == 2: + attributes = [("val", "double")] + elif underline == 33: + attributes = [("val", "singleAccounting")] + elif underline == 34: + attributes = [("val", "doubleAccounting")] + + self.rstring._xml_empty_tag("u", attributes) + + def _write_vert_align(self, val): + # Write the <vertAlign> font sub-element. + attributes = [("val", val)] + + self.rstring._xml_empty_tag("vertAlign", attributes) + + def _write_rstring_color(self, name, value): + # Write the <color> element. + attributes = [(name, value)] + + self.rstring._xml_empty_tag("color", attributes) + + def _get_palette_color(self, color): + # Convert the RGB color. + if color[0] == "#": + color = color[1:] + + return "FF" + color.upper() + + def _opt_close(self): + # Close the row data filehandle in constant_memory mode. + if not self.row_data_fh_closed: + self.row_data_fh.close() + self.row_data_fh_closed = True + + def _opt_reopen(self): + # Reopen the row data filehandle in constant_memory mode. + if self.row_data_fh_closed: + filename = self.row_data_filename + # pylint: disable=consider-using-with + self.row_data_fh = open(filename, mode="a+", encoding="utf-8") + self.row_data_fh_closed = False + self.fh = self.row_data_fh + + def _set_icon_props(self, total_icons, user_props=None): + # Set the sub-properties for icons. + props = [] + + # Set the defaults. + for _ in range(total_icons): + props.append({"criteria": False, "value": 0, "type": "percent"}) + + # Set the default icon values based on the number of icons. + if total_icons == 3: + props[0]["value"] = 67 + props[1]["value"] = 33 + + if total_icons == 4: + props[0]["value"] = 75 + props[1]["value"] = 50 + props[2]["value"] = 25 + + if total_icons == 5: + props[0]["value"] = 80 + props[1]["value"] = 60 + props[2]["value"] = 40 + props[3]["value"] = 20 + + # Overwrite default properties with user defined properties. + if user_props: + # Ensure we don't set user properties for lowest icon. + max_data = len(user_props) + if max_data >= total_icons: + max_data = total_icons - 1 + + for i in range(max_data): + # Set the user defined 'value' property. + if user_props[i].get("value") is not None: + props[i]["value"] = user_props[i]["value"] + + # Remove the formula '=' sign if it exists. + tmp = props[i]["value"] + if isinstance(tmp, str) and tmp.startswith("="): + props[i]["value"] = tmp.lstrip("=") + + # Set the user defined 'type' property. + if user_props[i].get("type"): + valid_types = ("percent", "percentile", "number", "formula") + + if user_props[i]["type"] not in valid_types: + warn( + f"Unknown icon property type '{user_props[i]['type']}' " + f"for sub-property 'type' in conditional_format()." + ) + else: + props[i]["type"] = user_props[i]["type"] + + if props[i]["type"] == "number": + props[i]["type"] = "num" + + # Set the user defined 'criteria' property. + criteria = user_props[i].get("criteria") + if criteria and criteria == ">": + props[i]["criteria"] = True + + return props + + ########################################################################### + # + # XML methods. + # + ########################################################################### + + def _write_worksheet(self): + # Write the <worksheet> element. This is the root element. + + schema = "http://schemas.openxmlformats.org/" + xmlns = schema + "spreadsheetml/2006/main" + xmlns_r = schema + "officeDocument/2006/relationships" + xmlns_mc = schema + "markup-compatibility/2006" + ms_schema = "http://schemas.microsoft.com/" + xmlns_x14ac = ms_schema + "office/spreadsheetml/2009/9/ac" + + attributes = [("xmlns", xmlns), ("xmlns:r", xmlns_r)] + + # Add some extra attributes for Excel 2010. Mainly for sparklines. + if self.excel_version == 2010: + attributes.append(("xmlns:mc", xmlns_mc)) + attributes.append(("xmlns:x14ac", xmlns_x14ac)) + attributes.append(("mc:Ignorable", "x14ac")) + + self._xml_start_tag("worksheet", attributes) + + def _write_dimension(self): + # Write the <dimension> element. This specifies the range of + # cells in the worksheet. As a special case, empty + # spreadsheets use 'A1' as a range. + + if self.dim_rowmin is None and self.dim_colmin is None: + # If the min dimensions are not defined then no dimensions + # have been set and we use the default 'A1'. + ref = "A1" + + elif self.dim_rowmin is None and self.dim_colmin is not None: + # If the row dimensions aren't set but the column + # dimensions are set then they have been changed via + # set_column(). + + if self.dim_colmin == self.dim_colmax: + # The dimensions are a single cell and not a range. + ref = xl_rowcol_to_cell(0, self.dim_colmin) + else: + # The dimensions are a cell range. + cell_1 = xl_rowcol_to_cell(0, self.dim_colmin) + cell_2 = xl_rowcol_to_cell(0, self.dim_colmax) + ref = cell_1 + ":" + cell_2 + + elif self.dim_rowmin == self.dim_rowmax and self.dim_colmin == self.dim_colmax: + # The dimensions are a single cell and not a range. + ref = xl_rowcol_to_cell(self.dim_rowmin, self.dim_colmin) + else: + # The dimensions are a cell range. + cell_1 = xl_rowcol_to_cell(self.dim_rowmin, self.dim_colmin) + cell_2 = xl_rowcol_to_cell(self.dim_rowmax, self.dim_colmax) + ref = cell_1 + ":" + cell_2 + + self._xml_empty_tag("dimension", [("ref", ref)]) + + def _write_sheet_views(self): + # Write the <sheetViews> element. + self._xml_start_tag("sheetViews") + + # Write the sheetView element. + self._write_sheet_view() + + self._xml_end_tag("sheetViews") + + def _write_sheet_view(self): + # Write the <sheetViews> element. + attributes = [] + + # Hide screen gridlines if required. + if not self.screen_gridlines: + attributes.append(("showGridLines", 0)) + + # Hide screen row/column headers. + if self.row_col_headers: + attributes.append(("showRowColHeaders", 0)) + + # Hide zeroes in cells. + if not self.show_zeros: + attributes.append(("showZeros", 0)) + + # Display worksheet right to left for Hebrew, Arabic and others. + if self.is_right_to_left: + attributes.append(("rightToLeft", 1)) + + # Show that the sheet tab is selected. + if self.selected: + attributes.append(("tabSelected", 1)) + + # Turn outlines off. Also required in the outlinePr element. + if not self.outline_on: + attributes.append(("showOutlineSymbols", 0)) + + # Set the page view/layout mode if required. + if self.page_view == 1: + attributes.append(("view", "pageLayout")) + elif self.page_view == 2: + attributes.append(("view", "pageBreakPreview")) + + # Set the first visible cell. + if self.top_left_cell != "": + attributes.append(("topLeftCell", self.top_left_cell)) + + # Set the zoom level. + if self.zoom != 100: + attributes.append(("zoomScale", self.zoom)) + + if self.page_view == 0 and self.zoom_scale_normal: + attributes.append(("zoomScaleNormal", self.zoom)) + if self.page_view == 1: + attributes.append(("zoomScalePageLayoutView", self.zoom)) + if self.page_view == 2: + attributes.append(("zoomScaleSheetLayoutView", self.zoom)) + + attributes.append(("workbookViewId", 0)) + + if self.panes or self.selections: + self._xml_start_tag("sheetView", attributes) + self._write_panes() + self._write_selections() + self._xml_end_tag("sheetView") + else: + self._xml_empty_tag("sheetView", attributes) + + def _write_sheet_format_pr(self): + # Write the <sheetFormatPr> element. + default_row_height = self.default_row_height + row_level = self.outline_row_level + col_level = self.outline_col_level + + attributes = [("defaultRowHeight", default_row_height)] + + if self.default_row_height != self.original_row_height: + attributes.append(("customHeight", 1)) + + if self.default_row_zeroed: + attributes.append(("zeroHeight", 1)) + + if row_level: + attributes.append(("outlineLevelRow", row_level)) + if col_level: + attributes.append(("outlineLevelCol", col_level)) + + if self.excel_version == 2010: + attributes.append(("x14ac:dyDescent", "0.25")) + + self._xml_empty_tag("sheetFormatPr", attributes) + + def _write_cols(self): + # Write the <cols> element and <col> sub elements. + + # Exit unless some column have been formatted. + if not self.col_info: + return + + self._xml_start_tag("cols") + + # Use the first element of the column information structures to set + # the initial/previous properties. + first_col = (sorted(self.col_info.keys()))[0] + last_col = first_col + prev_col_options = self.col_info[first_col] + del self.col_info[first_col] + deleted_col = first_col + deleted_col_options = prev_col_options + + for col in sorted(self.col_info.keys()): + col_options = self.col_info[col] + # Check if the column number is contiguous with the previous + # column and if the properties are the same. + if col == last_col + 1 and col_options == prev_col_options: + last_col = col + else: + # If not contiguous/equal then we write out the current range + # of columns and start again. + self._write_col_info(first_col, last_col, prev_col_options) + first_col = col + last_col = first_col + prev_col_options = col_options + + # We will exit the previous loop with one unhandled column range. + self._write_col_info(first_col, last_col, prev_col_options) + + # Put back the deleted first column information structure. + self.col_info[deleted_col] = deleted_col_options + + self._xml_end_tag("cols") + + def _write_col_info(self, col_min, col_max, col_info): + # Write the <col> element. + (width, cell_format, hidden, level, collapsed, autofit) = col_info + + custom_width = 1 + xf_index = 0 + + # Get the cell_format index. + if cell_format: + xf_index = cell_format._get_xf_index() + + # Set the Excel default column width. + if width is None: + if not hidden: + width = 8.43 + custom_width = 0 + else: + width = 0 + elif width == 8.43: + # Width is defined but same as default. + custom_width = 0 + + # Convert column width from user units to character width. + if width > 0: + # For Calabri 11. + max_digit_width = 7 + padding = 5 + + if width < 1: + width = ( + int( + (int(width * (max_digit_width + padding) + 0.5)) + / float(max_digit_width) + * 256.0 + ) + / 256.0 + ) + else: + width = ( + int( + (int(width * max_digit_width + 0.5) + padding) + / float(max_digit_width) + * 256.0 + ) + / 256.0 + ) + + attributes = [ + ("min", col_min + 1), + ("max", col_max + 1), + ("width", f"{width:.16g}"), + ] + + if xf_index: + attributes.append(("style", xf_index)) + if hidden: + attributes.append(("hidden", "1")) + if autofit: + attributes.append(("bestFit", "1")) + if custom_width: + attributes.append(("customWidth", "1")) + if level: + attributes.append(("outlineLevel", level)) + if collapsed: + attributes.append(("collapsed", "1")) + + self._xml_empty_tag("col", attributes) + + def _write_sheet_data(self): + # Write the <sheetData> element. + if self.dim_rowmin is None: + # If the dimensions aren't defined there is no data to write. + self._xml_empty_tag("sheetData") + else: + self._xml_start_tag("sheetData") + self._write_rows() + self._xml_end_tag("sheetData") + + def _write_optimized_sheet_data(self): + # Write the <sheetData> element when constant_memory is on. In this + # case we read the data stored in the temp file and rewrite it to the + # XML sheet file. + if self.dim_rowmin is None: + # If the dimensions aren't defined then there is no data to write. + self._xml_empty_tag("sheetData") + else: + self._xml_start_tag("sheetData") + + # Rewind the filehandle that was used for temp row data. + buff_size = 65536 + self.row_data_fh.seek(0) + data = self.row_data_fh.read(buff_size) + + while data: + self.fh.write(data) + data = self.row_data_fh.read(buff_size) + + self.row_data_fh.close() + os.unlink(self.row_data_filename) + + self._xml_end_tag("sheetData") + + def _write_page_margins(self): + # Write the <pageMargins> element. + attributes = [ + ("left", self.margin_left), + ("right", self.margin_right), + ("top", self.margin_top), + ("bottom", self.margin_bottom), + ("header", self.margin_header), + ("footer", self.margin_footer), + ] + + self._xml_empty_tag("pageMargins", attributes) + + def _write_page_setup(self): + # Write the <pageSetup> element. + # + # The following is an example taken from Excel. + # + # <pageSetup + # paperSize="9" + # scale="110" + # fitToWidth="2" + # fitToHeight="2" + # pageOrder="overThenDown" + # orientation="portrait" + # blackAndWhite="1" + # draft="1" + # horizontalDpi="200" + # verticalDpi="200" + # r:id="rId1" + # /> + # + attributes = [] + + # Skip this element if no page setup has changed. + if not self.page_setup_changed: + return + + # Set paper size. + if self.paper_size: + attributes.append(("paperSize", self.paper_size)) + + # Set the print_scale. + if self.print_scale != 100: + attributes.append(("scale", self.print_scale)) + + # Set the "Fit to page" properties. + if self.fit_page and self.fit_width != 1: + attributes.append(("fitToWidth", self.fit_width)) + + if self.fit_page and self.fit_height != 1: + attributes.append(("fitToHeight", self.fit_height)) + + # Set the page print direction. + if self.page_order: + attributes.append(("pageOrder", "overThenDown")) + + # Set start page for printing. + if self.page_start > 1: + attributes.append(("firstPageNumber", self.page_start)) + + # Set page orientation. + if self.orientation: + attributes.append(("orientation", "portrait")) + else: + attributes.append(("orientation", "landscape")) + + # Set the print in black and white option. + if self.black_white: + attributes.append(("blackAndWhite", "1")) + + # Set start page for printing. + if self.page_start != 0: + attributes.append(("useFirstPageNumber", "1")) + + # Set the DPI. Mainly only for testing. + if self.is_chartsheet: + if self.horizontal_dpi: + attributes.append(("horizontalDpi", self.horizontal_dpi)) + + if self.vertical_dpi: + attributes.append(("verticalDpi", self.vertical_dpi)) + else: + if self.vertical_dpi: + attributes.append(("verticalDpi", self.vertical_dpi)) + + if self.horizontal_dpi: + attributes.append(("horizontalDpi", self.horizontal_dpi)) + + self._xml_empty_tag("pageSetup", attributes) + + def _write_print_options(self): + # Write the <printOptions> element. + attributes = [] + + if not self.print_options_changed: + return + + # Set horizontal centering. + if self.hcenter: + attributes.append(("horizontalCentered", 1)) + + # Set vertical centering. + if self.vcenter: + attributes.append(("verticalCentered", 1)) + + # Enable row and column headers. + if self.print_headers: + attributes.append(("headings", 1)) + + # Set printed gridlines. + if self.print_gridlines: + attributes.append(("gridLines", 1)) + + self._xml_empty_tag("printOptions", attributes) + + def _write_header_footer(self): + # Write the <headerFooter> element. + attributes = [] + + if not self.header_footer_scales: + attributes.append(("scaleWithDoc", 0)) + + if not self.header_footer_aligns: + attributes.append(("alignWithMargins", 0)) + + if self.header_footer_changed: + self._xml_start_tag("headerFooter", attributes) + if self.header: + self._write_odd_header() + if self.footer: + self._write_odd_footer() + self._xml_end_tag("headerFooter") + elif self.excel2003_style: + self._xml_empty_tag("headerFooter", attributes) + + def _write_odd_header(self): + # Write the <headerFooter> element. + self._xml_data_element("oddHeader", self.header) + + def _write_odd_footer(self): + # Write the <headerFooter> element. + self._xml_data_element("oddFooter", self.footer) + + def _write_rows(self): + # Write out the worksheet data as a series of rows and cells. + self._calculate_spans() + + for row_num in range(self.dim_rowmin, self.dim_rowmax + 1): + if ( + row_num in self.set_rows + or row_num in self.comments + or self.table[row_num] + ): + # Only process rows with formatting, cell data and/or comments. + + span_index = int(row_num / 16) + + if span_index in self.row_spans: + span = self.row_spans[span_index] + else: + span = None + + if self.table[row_num]: + # Write the cells if the row contains data. + if row_num not in self.set_rows: + self._write_row(row_num, span) + else: + self._write_row(row_num, span, self.set_rows[row_num]) + + for col_num in range(self.dim_colmin, self.dim_colmax + 1): + if col_num in self.table[row_num]: + col_ref = self.table[row_num][col_num] + self._write_cell(row_num, col_num, col_ref) + + self._xml_end_tag("row") + + elif row_num in self.comments: + # Row with comments in cells. + self._write_empty_row(row_num, span, self.set_rows[row_num]) + else: + # Blank row with attributes only. + self._write_empty_row(row_num, span, self.set_rows[row_num]) + + def _write_single_row(self, current_row_num=0): + # Write out the worksheet data as a single row with cells. + # This method is used when constant_memory is on. A single + # row is written and the data table is reset. That way only + # one row of data is kept in memory at any one time. We don't + # write span data in the optimized case since it is optional. + + # Set the new previous row as the current row. + row_num = self.previous_row + self.previous_row = current_row_num + + if row_num in self.set_rows or row_num in self.comments or self.table[row_num]: + # Only process rows with formatting, cell data and/or comments. + + # No span data in optimized mode. + span = None + + if self.table[row_num]: + # Write the cells if the row contains data. + if row_num not in self.set_rows: + self._write_row(row_num, span) + else: + self._write_row(row_num, span, self.set_rows[row_num]) + + for col_num in range(self.dim_colmin, self.dim_colmax + 1): + if col_num in self.table[row_num]: + col_ref = self.table[row_num][col_num] + self._write_cell(row_num, col_num, col_ref) + + self._xml_end_tag("row") + else: + # Row attributes or comments only. + self._write_empty_row(row_num, span, self.set_rows[row_num]) + + # Reset table. + self.table.clear() + + def _calculate_spans(self): + # Calculate the "spans" attribute of the <row> tag. This is an + # XLSX optimization and isn't strictly required. However, it + # makes comparing files easier. The span is the same for each + # block of 16 rows. + spans = {} + span_min = None + span_max = None + + for row_num in range(self.dim_rowmin, self.dim_rowmax + 1): + if row_num in self.table: + # Calculate spans for cell data. + for col_num in range(self.dim_colmin, self.dim_colmax + 1): + if col_num in self.table[row_num]: + if span_min is None: + span_min = col_num + span_max = col_num + else: + span_min = min(span_min, col_num) + span_max = max(span_max, col_num) + + if row_num in self.comments: + # Calculate spans for comments. + for col_num in range(self.dim_colmin, self.dim_colmax + 1): + if row_num in self.comments and col_num in self.comments[row_num]: + if span_min is None: + span_min = col_num + span_max = col_num + else: + span_min = min(span_min, col_num) + span_max = max(span_max, col_num) + + if ((row_num + 1) % 16 == 0) or row_num == self.dim_rowmax: + span_index = int(row_num / 16) + + if span_min is not None: + span_min += 1 + span_max += 1 + spans[span_index] = f"{span_min}:{span_max}" + span_min = None + + self.row_spans = spans + + def _write_row(self, row, spans, properties=None, empty_row=False): + # Write the <row> element. + xf_index = 0 + + if properties: + height, cell_format, hidden, level, collapsed = properties + else: + height, cell_format, hidden, level, collapsed = None, None, 0, 0, 0 + + if height is None: + height = self.default_row_height + + attributes = [("r", row + 1)] + + # Get the cell_format index. + if cell_format: + xf_index = cell_format._get_xf_index() + + # Add row attributes where applicable. + if spans: + attributes.append(("spans", spans)) + + if xf_index: + attributes.append(("s", xf_index)) + + if cell_format: + attributes.append(("customFormat", 1)) + + if height != self.original_row_height or ( + height == self.original_row_height and height != self.default_row_height + ): + attributes.append(("ht", f"{height:g}")) + + if hidden: + attributes.append(("hidden", 1)) + + if height != self.original_row_height or ( + height == self.original_row_height and height != self.default_row_height + ): + attributes.append(("customHeight", 1)) + + if level: + attributes.append(("outlineLevel", level)) + + if collapsed: + attributes.append(("collapsed", 1)) + + if self.excel_version == 2010: + attributes.append(("x14ac:dyDescent", "0.25")) + + if empty_row: + self._xml_empty_tag_unencoded("row", attributes) + else: + self._xml_start_tag_unencoded("row", attributes) + + def _write_empty_row(self, row, spans, properties=None): + # Write and empty <row> element. + self._write_row(row, spans, properties, empty_row=True) + + def _write_cell(self, row, col, cell): + # Write the <cell> element. + # Note. This is the innermost loop so efficiency is important. + + cell_range = xl_rowcol_to_cell_fast(row, col) + attributes = [("r", cell_range)] + + if cell.format: + # Add the cell format index. + xf_index = cell.format._get_xf_index() + attributes.append(("s", xf_index)) + elif row in self.set_rows and self.set_rows[row][1]: + # Add the row format. + row_xf = self.set_rows[row][1] + attributes.append(("s", row_xf._get_xf_index())) + elif col in self.col_info: + # Add the column format. + col_xf = self.col_info[col][1] + if col_xf is not None: + attributes.append(("s", col_xf._get_xf_index())) + + type_cell_name = cell.__class__.__name__ + + # Write the various cell types. + if type_cell_name in ("Number", "Datetime"): + # Write a number. + self._xml_number_element(cell.number, attributes) + + elif type_cell_name in ("String", "RichString"): + # Write a string. + string = cell.string + + if not self.constant_memory: + # Write a shared string. + self._xml_string_element(string, attributes) + else: + # Write an optimized in-line string. + + # Convert control character to a _xHHHH_ escape. + string = self._escape_control_characters(string) + + # Write any rich strings without further tags. + if string.startswith("<r>") and string.endswith("</r>"): + self._xml_rich_inline_string(string, attributes) + else: + # Add attribute to preserve leading or trailing whitespace. + preserve = _preserve_whitespace(string) + self._xml_inline_string(string, preserve, attributes) + + elif type_cell_name == "Formula": + # Write a formula. First check the formula value type. + value = cell.value + if isinstance(cell.value, bool): + attributes.append(("t", "b")) + if cell.value: + value = 1 + else: + value = 0 + + elif isinstance(cell.value, str): + error_codes = ( + "#DIV/0!", + "#N/A", + "#NAME?", + "#NULL!", + "#NUM!", + "#REF!", + "#VALUE!", + ) + + if cell.value == "": + # Allow blank to force recalc in some third party apps. + pass + elif cell.value in error_codes: + attributes.append(("t", "e")) + else: + attributes.append(("t", "str")) + + self._xml_formula_element(cell.formula, value, attributes) + + elif type_cell_name == "ArrayFormula": + # Write a array formula. + + if cell.atype == "dynamic": + attributes.append(("cm", 1)) + + # First check if the formula value is a string. + try: + float(cell.value) + except ValueError: + attributes.append(("t", "str")) + + # Write an array formula. + self._xml_start_tag("c", attributes) + + self._write_cell_array_formula(cell.formula, cell.range) + self._write_cell_value(cell.value) + self._xml_end_tag("c") + + elif type_cell_name == "Blank": + # Write a empty cell. + self._xml_empty_tag("c", attributes) + + elif type_cell_name == "Boolean": + # Write a boolean cell. + attributes.append(("t", "b")) + self._xml_start_tag("c", attributes) + self._write_cell_value(cell.boolean) + self._xml_end_tag("c") + + elif type_cell_name == "Error": + # Write a boolean cell. + attributes.append(("t", "e")) + attributes.append(("vm", cell.value)) + self._xml_start_tag("c", attributes) + self._write_cell_value(cell.error) + self._xml_end_tag("c") + + def _write_cell_value(self, value): + # Write the cell value <v> element. + if value is None: + value = "" + + self._xml_data_element("v", value) + + def _write_cell_array_formula(self, formula, cell_range): + # Write the cell array formula <f> element. + attributes = [("t", "array"), ("ref", cell_range)] + + self._xml_data_element("f", formula, attributes) + + def _write_sheet_pr(self): + # Write the <sheetPr> element for Sheet level properties. + attributes = [] + + if ( + not self.fit_page + and not self.filter_on + and not self.tab_color + and not self.outline_changed + and not self.vba_codename + ): + return + + if self.vba_codename: + attributes.append(("codeName", self.vba_codename)) + + if self.filter_on: + attributes.append(("filterMode", 1)) + + if self.fit_page or self.tab_color or self.outline_changed: + self._xml_start_tag("sheetPr", attributes) + self._write_tab_color() + self._write_outline_pr() + self._write_page_set_up_pr() + self._xml_end_tag("sheetPr") + else: + self._xml_empty_tag("sheetPr", attributes) + + def _write_page_set_up_pr(self): + # Write the <pageSetUpPr> element. + if not self.fit_page: + return + + attributes = [("fitToPage", 1)] + self._xml_empty_tag("pageSetUpPr", attributes) + + def _write_tab_color(self): + # Write the <tabColor> element. + color = self.tab_color + + if not color: + return + + attributes = [("rgb", color)] + + self._xml_empty_tag("tabColor", attributes) + + def _write_outline_pr(self): + # Write the <outlinePr> element. + attributes = [] + + if not self.outline_changed: + return + + if self.outline_style: + attributes.append(("applyStyles", 1)) + if not self.outline_below: + attributes.append(("summaryBelow", 0)) + if not self.outline_right: + attributes.append(("summaryRight", 0)) + if not self.outline_on: + attributes.append(("showOutlineSymbols", 0)) + + self._xml_empty_tag("outlinePr", attributes) + + def _write_row_breaks(self): + # Write the <rowBreaks> element. + page_breaks = self._sort_pagebreaks(self.hbreaks) + + if not page_breaks: + return + + count = len(page_breaks) + + attributes = [ + ("count", count), + ("manualBreakCount", count), + ] + + self._xml_start_tag("rowBreaks", attributes) + + for row_num in page_breaks: + self._write_brk(row_num, 16383) + + self._xml_end_tag("rowBreaks") + + def _write_col_breaks(self): + # Write the <colBreaks> element. + page_breaks = self._sort_pagebreaks(self.vbreaks) + + if not page_breaks: + return + + count = len(page_breaks) + + attributes = [ + ("count", count), + ("manualBreakCount", count), + ] + + self._xml_start_tag("colBreaks", attributes) + + for col_num in page_breaks: + self._write_brk(col_num, 1048575) + + self._xml_end_tag("colBreaks") + + def _write_brk(self, brk_id, brk_max): + # Write the <brk> element. + attributes = [("id", brk_id), ("max", brk_max), ("man", 1)] + + self._xml_empty_tag("brk", attributes) + + def _write_merge_cells(self): + # Write the <mergeCells> element. + merged_cells = self.merge + count = len(merged_cells) + + if not count: + return + + attributes = [("count", count)] + + self._xml_start_tag("mergeCells", attributes) + + for merged_range in merged_cells: + # Write the mergeCell element. + self._write_merge_cell(merged_range) + + self._xml_end_tag("mergeCells") + + def _write_merge_cell(self, merged_range): + # Write the <mergeCell> element. + (row_min, col_min, row_max, col_max) = merged_range + + # Convert the merge dimensions to a cell range. + cell_1 = xl_rowcol_to_cell(row_min, col_min) + cell_2 = xl_rowcol_to_cell(row_max, col_max) + ref = cell_1 + ":" + cell_2 + + attributes = [("ref", ref)] + + self._xml_empty_tag("mergeCell", attributes) + + def _write_hyperlinks(self): + # Process any stored hyperlinks in row/col order and write the + # <hyperlinks> element. The attributes are different for internal + # and external links. + hlink_refs = [] + display = None + + # Sort the hyperlinks into row order. + row_nums = sorted(self.hyperlinks.keys()) + + # Exit if there are no hyperlinks to process. + if not row_nums: + return + + # Iterate over the rows. + for row_num in row_nums: + # Sort the hyperlinks into column order. + col_nums = sorted(self.hyperlinks[row_num].keys()) + + # Iterate over the columns. + for col_num in col_nums: + # Get the link data for this cell. + link = self.hyperlinks[row_num][col_num] + link_type = link["link_type"] + + # If the cell isn't a string then we have to add the url as + # the string to display. + if self.table and self.table[row_num] and self.table[row_num][col_num]: + cell = self.table[row_num][col_num] + if cell.__class__.__name__ != "String": + display = link["url"] + + if link_type == 1: + # External link with rel file relationship. + self.rel_count += 1 + + hlink_refs.append( + [ + link_type, + row_num, + col_num, + self.rel_count, + link["str"], + display, + link["tip"], + ] + ) + + # Links for use by the packager. + self.external_hyper_links.append( + ["/hyperlink", link["url"], "External"] + ) + else: + # Internal link with rel file relationship. + hlink_refs.append( + [ + link_type, + row_num, + col_num, + link["url"], + link["str"], + link["tip"], + ] + ) + + # Write the hyperlink elements. + self._xml_start_tag("hyperlinks") + + for args in hlink_refs: + link_type = args.pop(0) + + if link_type == 1: + self._write_hyperlink_external(*args) + elif link_type == 2: + self._write_hyperlink_internal(*args) + + self._xml_end_tag("hyperlinks") + + def _write_hyperlink_external( + self, row, col, id_num, location=None, display=None, tooltip=None + ): + # Write the <hyperlink> element for external links. + ref = xl_rowcol_to_cell(row, col) + r_id = "rId" + str(id_num) + + attributes = [("ref", ref), ("r:id", r_id)] + + if location is not None: + attributes.append(("location", location)) + if display is not None: + attributes.append(("display", display)) + if tooltip is not None: + attributes.append(("tooltip", tooltip)) + + self._xml_empty_tag("hyperlink", attributes) + + def _write_hyperlink_internal( + self, row, col, location=None, display=None, tooltip=None + ): + # Write the <hyperlink> element for internal links. + ref = xl_rowcol_to_cell(row, col) + + attributes = [("ref", ref), ("location", location)] + + if tooltip is not None: + attributes.append(("tooltip", tooltip)) + attributes.append(("display", display)) + + self._xml_empty_tag("hyperlink", attributes) + + def _write_auto_filter(self): + # Write the <autoFilter> element. + if not self.autofilter_ref: + return + + attributes = [("ref", self.autofilter_ref)] + + if self.filter_on: + # Autofilter defined active filters. + self._xml_start_tag("autoFilter", attributes) + self._write_autofilters() + self._xml_end_tag("autoFilter") + + else: + # Autofilter defined without active filters. + self._xml_empty_tag("autoFilter", attributes) + + def _write_autofilters(self): + # Function to iterate through the columns that form part of an + # autofilter range and write the appropriate filters. + (col1, col2) = self.filter_range + + for col in range(col1, col2 + 1): + # Skip if column doesn't have an active filter. + if col not in self.filter_cols: + continue + + # Retrieve the filter tokens and write the autofilter records. + tokens = self.filter_cols[col] + filter_type = self.filter_type[col] + + # Filters are relative to first column in the autofilter. + self._write_filter_column(col - col1, filter_type, tokens) + + def _write_filter_column(self, col_id, filter_type, filters): + # Write the <filterColumn> element. + attributes = [("colId", col_id)] + + self._xml_start_tag("filterColumn", attributes) + + if filter_type == 1: + # Type == 1 is the new XLSX style filter. + self._write_filters(filters) + else: + # Type == 0 is the classic "custom" filter. + self._write_custom_filters(filters) + + self._xml_end_tag("filterColumn") + + def _write_filters(self, filters): + # Write the <filters> element. + non_blanks = [filter for filter in filters if str(filter).lower() != "blanks"] + attributes = [] + + if len(filters) != len(non_blanks): + attributes = [("blank", 1)] + + if len(filters) == 1 and len(non_blanks) == 0: + # Special case for blank cells only. + self._xml_empty_tag("filters", attributes) + else: + # General case. + self._xml_start_tag("filters", attributes) + + for autofilter in sorted(non_blanks): + self._write_filter(autofilter) + + self._xml_end_tag("filters") + + def _write_filter(self, val): + # Write the <filter> element. + attributes = [("val", val)] + + self._xml_empty_tag("filter", attributes) + + def _write_custom_filters(self, tokens): + # Write the <customFilters> element. + if len(tokens) == 2: + # One filter expression only. + self._xml_start_tag("customFilters") + self._write_custom_filter(*tokens) + self._xml_end_tag("customFilters") + else: + # Two filter expressions. + attributes = [] + + # Check if the "join" operand is "and" or "or". + if tokens[2] == 0: + attributes = [("and", 1)] + else: + attributes = [("and", 0)] + + # Write the two custom filters. + self._xml_start_tag("customFilters", attributes) + self._write_custom_filter(tokens[0], tokens[1]) + self._write_custom_filter(tokens[3], tokens[4]) + self._xml_end_tag("customFilters") + + def _write_custom_filter(self, operator, val): + # Write the <customFilter> element. + attributes = [] + + operators = { + 1: "lessThan", + 2: "equal", + 3: "lessThanOrEqual", + 4: "greaterThan", + 5: "notEqual", + 6: "greaterThanOrEqual", + 22: "equal", + } + + # Convert the operator from a number to a descriptive string. + if operators[operator] is not None: + operator = operators[operator] + else: + warn(f"Unknown operator = {operator}") + + # The 'equal' operator is the default attribute and isn't stored. + if operator != "equal": + attributes.append(("operator", operator)) + attributes.append(("val", val)) + + self._xml_empty_tag("customFilter", attributes) + + def _write_sheet_protection(self): + # Write the <sheetProtection> element. + attributes = [] + + if not self.protect_options: + return + + options = self.protect_options + + if options["password"]: + attributes.append(("password", options["password"])) + if options["sheet"]: + attributes.append(("sheet", 1)) + if options["content"]: + attributes.append(("content", 1)) + if not options["objects"]: + attributes.append(("objects", 1)) + if not options["scenarios"]: + attributes.append(("scenarios", 1)) + if options["format_cells"]: + attributes.append(("formatCells", 0)) + if options["format_columns"]: + attributes.append(("formatColumns", 0)) + if options["format_rows"]: + attributes.append(("formatRows", 0)) + if options["insert_columns"]: + attributes.append(("insertColumns", 0)) + if options["insert_rows"]: + attributes.append(("insertRows", 0)) + if options["insert_hyperlinks"]: + attributes.append(("insertHyperlinks", 0)) + if options["delete_columns"]: + attributes.append(("deleteColumns", 0)) + if options["delete_rows"]: + attributes.append(("deleteRows", 0)) + if not options["select_locked_cells"]: + attributes.append(("selectLockedCells", 1)) + if options["sort"]: + attributes.append(("sort", 0)) + if options["autofilter"]: + attributes.append(("autoFilter", 0)) + if options["pivot_tables"]: + attributes.append(("pivotTables", 0)) + if not options["select_unlocked_cells"]: + attributes.append(("selectUnlockedCells", 1)) + + self._xml_empty_tag("sheetProtection", attributes) + + def _write_protected_ranges(self): + # Write the <protectedRanges> element. + if self.num_protected_ranges == 0: + return + + self._xml_start_tag("protectedRanges") + + for cell_range, range_name, password in self.protected_ranges: + self._write_protected_range(cell_range, range_name, password) + + self._xml_end_tag("protectedRanges") + + def _write_protected_range(self, cell_range, range_name, password): + # Write the <protectedRange> element. + attributes = [] + + if password: + attributes.append(("password", password)) + + attributes.append(("sqref", cell_range)) + attributes.append(("name", range_name)) + + self._xml_empty_tag("protectedRange", attributes) + + def _write_drawings(self): + # Write the <drawing> elements. + if not self.drawing: + return + + self.rel_count += 1 + self._write_drawing(self.rel_count) + + def _write_drawing(self, drawing_id): + # Write the <drawing> element. + r_id = "rId" + str(drawing_id) + + attributes = [("r:id", r_id)] + + self._xml_empty_tag("drawing", attributes) + + def _write_legacy_drawing(self): + # Write the <legacyDrawing> element. + if not self.has_vml: + return + + # Increment the relationship id for any drawings or comments. + self.rel_count += 1 + r_id = "rId" + str(self.rel_count) + + attributes = [("r:id", r_id)] + + self._xml_empty_tag("legacyDrawing", attributes) + + def _write_legacy_drawing_hf(self): + # Write the <legacyDrawingHF> element. + if not self.has_header_vml: + return + + # Increment the relationship id for any drawings or comments. + self.rel_count += 1 + r_id = "rId" + str(self.rel_count) + + attributes = [("r:id", r_id)] + + self._xml_empty_tag("legacyDrawingHF", attributes) + + def _write_picture(self): + # Write the <picture> element. + if not self.background_image: + return + + # Increment the relationship id. + self.rel_count += 1 + r_id = "rId" + str(self.rel_count) + + attributes = [("r:id", r_id)] + + self._xml_empty_tag("picture", attributes) + + def _write_data_validations(self): + # Write the <dataValidations> element. + validations = self.validations + count = len(validations) + + if not count: + return + + attributes = [("count", count)] + + self._xml_start_tag("dataValidations", attributes) + + for validation in validations: + # Write the dataValidation element. + self._write_data_validation(validation) + + self._xml_end_tag("dataValidations") + + def _write_data_validation(self, options): + # Write the <dataValidation> element. + sqref = "" + attributes = [] + + # Set the cell range(s) for the data validation. + for cells in options["cells"]: + # Add a space between multiple cell ranges. + if sqref != "": + sqref += " " + + (row_first, col_first, row_last, col_last) = cells + + # Swap last row/col for first row/col as necessary + if row_first > row_last: + (row_first, row_last) = (row_last, row_first) + + if col_first > col_last: + (col_first, col_last) = (col_last, col_first) + + sqref += xl_range(row_first, col_first, row_last, col_last) + + if options.get("multi_range"): + sqref = options["multi_range"] + + if options["validate"] != "none": + attributes.append(("type", options["validate"])) + + if options["criteria"] != "between": + attributes.append(("operator", options["criteria"])) + + if "error_type" in options: + if options["error_type"] == 1: + attributes.append(("errorStyle", "warning")) + if options["error_type"] == 2: + attributes.append(("errorStyle", "information")) + + if options["ignore_blank"]: + attributes.append(("allowBlank", 1)) + + if not options["dropdown"]: + attributes.append(("showDropDown", 1)) + + if options["show_input"]: + attributes.append(("showInputMessage", 1)) + + if options["show_error"]: + attributes.append(("showErrorMessage", 1)) + + if "error_title" in options: + attributes.append(("errorTitle", options["error_title"])) + + if "error_message" in options: + attributes.append(("error", options["error_message"])) + + if "input_title" in options: + attributes.append(("promptTitle", options["input_title"])) + + if "input_message" in options: + attributes.append(("prompt", options["input_message"])) + + attributes.append(("sqref", sqref)) + + if options["validate"] == "none": + self._xml_empty_tag("dataValidation", attributes) + else: + self._xml_start_tag("dataValidation", attributes) + + # Write the formula1 element. + self._write_formula_1(options["value"]) + + # Write the formula2 element. + if options["maximum"] is not None: + self._write_formula_2(options["maximum"]) + + self._xml_end_tag("dataValidation") + + def _write_formula_1(self, formula): + # Write the <formula1> element. + + if isinstance(formula, list): + formula = self._csv_join(*formula) + formula = f'"{formula}"' + else: + # Check if the formula is a number. + try: + float(formula) + except ValueError: + # Not a number. Remove the formula '=' sign if it exists. + if formula.startswith("="): + formula = formula.lstrip("=") + + self._xml_data_element("formula1", formula) + + def _write_formula_2(self, formula): + # Write the <formula2> element. + + # Check if the formula is a number. + try: + float(formula) + except ValueError: + # Not a number. Remove the formula '=' sign if it exists. + if formula.startswith("="): + formula = formula.lstrip("=") + + self._xml_data_element("formula2", formula) + + def _write_conditional_formats(self): + # Write the Worksheet conditional formats. + ranges = sorted(self.cond_formats.keys()) + + if not ranges: + return + + for cond_range in ranges: + self._write_conditional_formatting( + cond_range, self.cond_formats[cond_range] + ) + + def _write_conditional_formatting(self, cond_range, params): + # Write the <conditionalFormatting> element. + attributes = [("sqref", cond_range)] + self._xml_start_tag("conditionalFormatting", attributes) + for param in params: + # Write the cfRule element. + self._write_cf_rule(param) + self._xml_end_tag("conditionalFormatting") + + def _write_cf_rule(self, params): + # Write the <cfRule> element. + attributes = [("type", params["type"])] + + if "format" in params and params["format"] is not None: + attributes.append(("dxfId", params["format"])) + + attributes.append(("priority", params["priority"])) + + if params.get("stop_if_true"): + attributes.append(("stopIfTrue", 1)) + + if params["type"] == "cellIs": + attributes.append(("operator", params["criteria"])) + + self._xml_start_tag("cfRule", attributes) + + if "minimum" in params and "maximum" in params: + self._write_formula_element(params["minimum"]) + self._write_formula_element(params["maximum"]) + else: + self._write_formula_element(params["value"]) + + self._xml_end_tag("cfRule") + + elif params["type"] == "aboveAverage": + if re.search("below", params["criteria"]): + attributes.append(("aboveAverage", 0)) + + if re.search("equal", params["criteria"]): + attributes.append(("equalAverage", 1)) + + if re.search("[123] std dev", params["criteria"]): + match = re.search("([123]) std dev", params["criteria"]) + attributes.append(("stdDev", match.group(1))) + + self._xml_empty_tag("cfRule", attributes) + + elif params["type"] == "top10": + if "criteria" in params and params["criteria"] == "%": + attributes.append(("percent", 1)) + + if "direction" in params: + attributes.append(("bottom", 1)) + + rank = params["value"] or 10 + attributes.append(("rank", rank)) + + self._xml_empty_tag("cfRule", attributes) + + elif params["type"] == "duplicateValues": + self._xml_empty_tag("cfRule", attributes) + + elif params["type"] == "uniqueValues": + self._xml_empty_tag("cfRule", attributes) + + elif ( + params["type"] == "containsText" + or params["type"] == "notContainsText" + or params["type"] == "beginsWith" + or params["type"] == "endsWith" + ): + attributes.append(("operator", params["criteria"])) + attributes.append(("text", params["value"])) + self._xml_start_tag("cfRule", attributes) + self._write_formula_element(params["formula"]) + self._xml_end_tag("cfRule") + + elif params["type"] == "timePeriod": + attributes.append(("timePeriod", params["criteria"])) + self._xml_start_tag("cfRule", attributes) + self._write_formula_element(params["formula"]) + self._xml_end_tag("cfRule") + + elif ( + params["type"] == "containsBlanks" + or params["type"] == "notContainsBlanks" + or params["type"] == "containsErrors" + or params["type"] == "notContainsErrors" + ): + self._xml_start_tag("cfRule", attributes) + self._write_formula_element(params["formula"]) + self._xml_end_tag("cfRule") + + elif params["type"] == "colorScale": + self._xml_start_tag("cfRule", attributes) + self._write_color_scale(params) + self._xml_end_tag("cfRule") + + elif params["type"] == "dataBar": + self._xml_start_tag("cfRule", attributes) + self._write_data_bar(params) + + if params.get("is_data_bar_2010"): + self._write_data_bar_ext(params) + + self._xml_end_tag("cfRule") + + elif params["type"] == "expression": + self._xml_start_tag("cfRule", attributes) + self._write_formula_element(params["criteria"]) + self._xml_end_tag("cfRule") + + elif params["type"] == "iconSet": + self._xml_start_tag("cfRule", attributes) + self._write_icon_set(params) + self._xml_end_tag("cfRule") + + def _write_formula_element(self, formula): + # Write the <formula> element. + + # Check if the formula is a number. + try: + float(formula) + except ValueError: + # Not a number. Remove the formula '=' sign if it exists. + if formula.startswith("="): + formula = formula.lstrip("=") + + self._xml_data_element("formula", formula) + + def _write_color_scale(self, param): + # Write the <colorScale> element. + + self._xml_start_tag("colorScale") + + self._write_cfvo(param["min_type"], param["min_value"]) + + if param["mid_type"] is not None: + self._write_cfvo(param["mid_type"], param["mid_value"]) + + self._write_cfvo(param["max_type"], param["max_value"]) + + self._write_color("rgb", param["min_color"]) + + if param["mid_color"] is not None: + self._write_color("rgb", param["mid_color"]) + + self._write_color("rgb", param["max_color"]) + + self._xml_end_tag("colorScale") + + def _write_data_bar(self, param): + # Write the <dataBar> element. + attributes = [] + + # Min and max bar lengths in in the spec but not supported directly by + # Excel. + if "min_length" in param: + attributes.append(("minLength", param["min_length"])) + + if "max_length" in param: + attributes.append(("maxLength", param["max_length"])) + + if param.get("bar_only"): + attributes.append(("showValue", 0)) + + self._xml_start_tag("dataBar", attributes) + + self._write_cfvo(param["min_type"], param["min_value"]) + self._write_cfvo(param["max_type"], param["max_value"]) + self._write_color("rgb", param["bar_color"]) + + self._xml_end_tag("dataBar") + + def _write_data_bar_ext(self, param): + # Write the <extLst> dataBar extension element. + + # Create a pseudo GUID for each unique Excel 2010 data bar. + worksheet_count = self.index + 1 + data_bar_count = len(self.data_bars_2010) + 1 + guid = "{DA7ABA51-AAAA-BBBB-%04X-%012X}" % (worksheet_count, data_bar_count) + + # Store the 2010 data bar parameters to write the extLst elements. + param["guid"] = guid + self.data_bars_2010.append(param) + + self._xml_start_tag("extLst") + self._write_ext("{B025F937-C7B1-47D3-B67F-A62EFF666E3E}") + self._xml_data_element("x14:id", guid) + self._xml_end_tag("ext") + self._xml_end_tag("extLst") + + def _write_icon_set(self, param): + # Write the <iconSet> element. + attributes = [] + + # Don't set attribute for default style. + if param["icon_style"] != "3TrafficLights": + attributes = [("iconSet", param["icon_style"])] + + if param.get("icons_only"): + attributes.append(("showValue", 0)) + + if param.get("reverse_icons"): + attributes.append(("reverse", 1)) + + self._xml_start_tag("iconSet", attributes) + + # Write the properties for different icon styles. + for icon in reversed(param["icons"]): + self._write_cfvo(icon["type"], icon["value"], icon["criteria"]) + + self._xml_end_tag("iconSet") + + def _write_cfvo(self, cf_type, val, criteria=None): + # Write the <cfvo> element. + attributes = [("type", cf_type)] + + if val is not None: + attributes.append(("val", val)) + + if criteria: + attributes.append(("gte", 0)) + + self._xml_empty_tag("cfvo", attributes) + + def _write_color(self, name, value): + # Write the <color> element. + attributes = [(name, value)] + + self._xml_empty_tag("color", attributes) + + def _write_selections(self): + # Write the <selection> elements. + for selection in self.selections: + self._write_selection(*selection) + + def _write_selection(self, pane, active_cell, sqref): + # Write the <selection> element. + attributes = [] + + if pane: + attributes.append(("pane", pane)) + + if active_cell: + attributes.append(("activeCell", active_cell)) + + if sqref: + attributes.append(("sqref", sqref)) + + self._xml_empty_tag("selection", attributes) + + def _write_panes(self): + # Write the frozen or split <pane> elements. + panes = self.panes + + if not panes: + return + + if panes[4] == 2: + self._write_split_panes(*panes) + else: + self._write_freeze_panes(*panes) + + def _write_freeze_panes(self, row, col, top_row, left_col, pane_type): + # Write the <pane> element for freeze panes. + attributes = [] + + y_split = row + x_split = col + top_left_cell = xl_rowcol_to_cell(top_row, left_col) + active_pane = "" + state = "" + active_cell = "" + sqref = "" + + # Move user cell selection to the panes. + if self.selections: + (_, active_cell, sqref) = self.selections[0] + self.selections = [] + + # Set the active pane. + if row and col: + active_pane = "bottomRight" + + row_cell = xl_rowcol_to_cell(row, 0) + col_cell = xl_rowcol_to_cell(0, col) + + self.selections.append(["topRight", col_cell, col_cell]) + self.selections.append(["bottomLeft", row_cell, row_cell]) + self.selections.append(["bottomRight", active_cell, sqref]) + + elif col: + active_pane = "topRight" + self.selections.append(["topRight", active_cell, sqref]) + + else: + active_pane = "bottomLeft" + self.selections.append(["bottomLeft", active_cell, sqref]) + + # Set the pane type. + if pane_type == 0: + state = "frozen" + elif pane_type == 1: + state = "frozenSplit" + else: + state = "split" + + if x_split: + attributes.append(("xSplit", x_split)) + + if y_split: + attributes.append(("ySplit", y_split)) + + attributes.append(("topLeftCell", top_left_cell)) + attributes.append(("activePane", active_pane)) + attributes.append(("state", state)) + + self._xml_empty_tag("pane", attributes) + + def _write_split_panes(self, row, col, top_row, left_col, _): + # Write the <pane> element for split panes. + attributes = [] + has_selection = 0 + active_pane = "" + active_cell = "" + sqref = "" + + y_split = row + x_split = col + + # Move user cell selection to the panes. + if self.selections: + (_, active_cell, sqref) = self.selections[0] + self.selections = [] + has_selection = 1 + + # Convert the row and col to 1/20 twip units with padding. + if y_split: + y_split = int(20 * y_split + 300) + + if x_split: + x_split = self._calculate_x_split_width(x_split) + + # For non-explicit topLeft definitions, estimate the cell offset based + # on the pixels dimensions. This is only a workaround and doesn't take + # adjusted cell dimensions into account. + if top_row == row and left_col == col: + top_row = int(0.5 + (y_split - 300) / 20 / 15) + left_col = int(0.5 + (x_split - 390) / 20 / 3 * 4 / 64) + + top_left_cell = xl_rowcol_to_cell(top_row, left_col) + + # If there is no selection set the active cell to the top left cell. + if not has_selection: + active_cell = top_left_cell + sqref = top_left_cell + + # Set the Cell selections. + if row and col: + active_pane = "bottomRight" + + row_cell = xl_rowcol_to_cell(top_row, 0) + col_cell = xl_rowcol_to_cell(0, left_col) + + self.selections.append(["topRight", col_cell, col_cell]) + self.selections.append(["bottomLeft", row_cell, row_cell]) + self.selections.append(["bottomRight", active_cell, sqref]) + + elif col: + active_pane = "topRight" + self.selections.append(["topRight", active_cell, sqref]) + + else: + active_pane = "bottomLeft" + self.selections.append(["bottomLeft", active_cell, sqref]) + + # Format splits to the same precision as Excel. + if x_split: + attributes.append(("xSplit", f"{x_split:.16g}")) + + if y_split: + attributes.append(("ySplit", f"{y_split:.16g}")) + + attributes.append(("topLeftCell", top_left_cell)) + + if has_selection: + attributes.append(("activePane", active_pane)) + + self._xml_empty_tag("pane", attributes) + + def _calculate_x_split_width(self, width): + # Convert column width from user units to pane split width. + + max_digit_width = 7 # For Calabri 11. + padding = 5 + + # Convert to pixels. + if width < 1: + pixels = int(width * (max_digit_width + padding) + 0.5) + else: + pixels = int(width * max_digit_width + 0.5) + padding + + # Convert to points. + points = pixels * 3 / 4 + + # Convert to twips (twentieths of a point). + twips = points * 20 + + # Add offset/padding. + width = twips + 390 + + return width + + def _write_table_parts(self): + # Write the <tableParts> element. + tables = self.tables + count = len(tables) + + # Return if worksheet doesn't contain any tables. + if not count: + return + + attributes = [ + ( + "count", + count, + ) + ] + + self._xml_start_tag("tableParts", attributes) + + for _ in tables: + # Write the tablePart element. + self.rel_count += 1 + self._write_table_part(self.rel_count) + + self._xml_end_tag("tableParts") + + def _write_table_part(self, r_id): + # Write the <tablePart> element. + + r_id = "rId" + str(r_id) + + attributes = [ + ( + "r:id", + r_id, + ) + ] + + self._xml_empty_tag("tablePart", attributes) + + def _write_ext_list(self): + # Write the <extLst> element for data bars and sparklines. + has_data_bars = len(self.data_bars_2010) + has_sparklines = len(self.sparklines) + + if not has_data_bars and not has_sparklines: + return + + # Write the extLst element. + self._xml_start_tag("extLst") + + if has_data_bars: + self._write_ext_list_data_bars() + + if has_sparklines: + self._write_ext_list_sparklines() + + self._xml_end_tag("extLst") + + def _write_ext_list_data_bars(self): + # Write the Excel 2010 data_bar subelements. + self._write_ext("{78C0D931-6437-407d-A8EE-F0AAD7539E65}") + + self._xml_start_tag("x14:conditionalFormattings") + + # Write the Excel 2010 conditional formatting data bar elements. + for data_bar in self.data_bars_2010: + # Write the x14:conditionalFormatting element. + self._write_conditional_formatting_2010(data_bar) + + self._xml_end_tag("x14:conditionalFormattings") + self._xml_end_tag("ext") + + def _write_conditional_formatting_2010(self, data_bar): + # Write the <x14:conditionalFormatting> element. + xmlns_xm = "http://schemas.microsoft.com/office/excel/2006/main" + + attributes = [("xmlns:xm", xmlns_xm)] + + self._xml_start_tag("x14:conditionalFormatting", attributes) + + # Write the x14:cfRule element. + self._write_x14_cf_rule(data_bar) + + # Write the x14:dataBar element. + self._write_x14_data_bar(data_bar) + + # Write the x14 max and min data bars. + self._write_x14_cfvo(data_bar["x14_min_type"], data_bar["min_value"]) + self._write_x14_cfvo(data_bar["x14_max_type"], data_bar["max_value"]) + + if not data_bar["bar_no_border"]: + # Write the x14:borderColor element. + self._write_x14_border_color(data_bar["bar_border_color"]) + + # Write the x14:negativeFillColor element. + if not data_bar["bar_negative_color_same"]: + self._write_x14_negative_fill_color(data_bar["bar_negative_color"]) + + # Write the x14:negativeBorderColor element. + if ( + not data_bar["bar_no_border"] + and not data_bar["bar_negative_border_color_same"] + ): + self._write_x14_negative_border_color(data_bar["bar_negative_border_color"]) + + # Write the x14:axisColor element. + if data_bar["bar_axis_position"] != "none": + self._write_x14_axis_color(data_bar["bar_axis_color"]) + + self._xml_end_tag("x14:dataBar") + self._xml_end_tag("x14:cfRule") + + # Write the xm:sqref element. + self._xml_data_element("xm:sqref", data_bar["range"]) + + self._xml_end_tag("x14:conditionalFormatting") + + def _write_x14_cf_rule(self, data_bar): + # Write the <x14:cfRule> element. + rule_type = "dataBar" + guid = data_bar["guid"] + attributes = [("type", rule_type), ("id", guid)] + + self._xml_start_tag("x14:cfRule", attributes) + + def _write_x14_data_bar(self, data_bar): + # Write the <x14:dataBar> element. + min_length = 0 + max_length = 100 + + attributes = [ + ("minLength", min_length), + ("maxLength", max_length), + ] + + if not data_bar["bar_no_border"]: + attributes.append(("border", 1)) + + if data_bar["bar_solid"]: + attributes.append(("gradient", 0)) + + if data_bar["bar_direction"] == "left": + attributes.append(("direction", "leftToRight")) + + if data_bar["bar_direction"] == "right": + attributes.append(("direction", "rightToLeft")) + + if data_bar["bar_negative_color_same"]: + attributes.append(("negativeBarColorSameAsPositive", 1)) + + if ( + not data_bar["bar_no_border"] + and not data_bar["bar_negative_border_color_same"] + ): + attributes.append(("negativeBarBorderColorSameAsPositive", 0)) + + if data_bar["bar_axis_position"] == "middle": + attributes.append(("axisPosition", "middle")) + + if data_bar["bar_axis_position"] == "none": + attributes.append(("axisPosition", "none")) + + self._xml_start_tag("x14:dataBar", attributes) + + def _write_x14_cfvo(self, rule_type, value): + # Write the <x14:cfvo> element. + attributes = [("type", rule_type)] + + if rule_type in ("min", "max", "autoMin", "autoMax"): + self._xml_empty_tag("x14:cfvo", attributes) + else: + self._xml_start_tag("x14:cfvo", attributes) + self._xml_data_element("xm:f", value) + self._xml_end_tag("x14:cfvo") + + def _write_x14_border_color(self, rgb): + # Write the <x14:borderColor> element. + attributes = [("rgb", rgb)] + self._xml_empty_tag("x14:borderColor", attributes) + + def _write_x14_negative_fill_color(self, rgb): + # Write the <x14:negativeFillColor> element. + attributes = [("rgb", rgb)] + self._xml_empty_tag("x14:negativeFillColor", attributes) + + def _write_x14_negative_border_color(self, rgb): + # Write the <x14:negativeBorderColor> element. + attributes = [("rgb", rgb)] + self._xml_empty_tag("x14:negativeBorderColor", attributes) + + def _write_x14_axis_color(self, rgb): + # Write the <x14:axisColor> element. + attributes = [("rgb", rgb)] + self._xml_empty_tag("x14:axisColor", attributes) + + def _write_ext_list_sparklines(self): + # Write the sparkline extension sub-elements. + self._write_ext("{05C60535-1F16-4fd2-B633-F4F36F0B64E0}") + + # Write the x14:sparklineGroups element. + self._write_sparkline_groups() + + # Write the sparkline elements. + for sparkline in reversed(self.sparklines): + # Write the x14:sparklineGroup element. + self._write_sparkline_group(sparkline) + + # Write the x14:colorSeries element. + self._write_color_series(sparkline["series_color"]) + + # Write the x14:colorNegative element. + self._write_color_negative(sparkline["negative_color"]) + + # Write the x14:colorAxis element. + self._write_color_axis() + + # Write the x14:colorMarkers element. + self._write_color_markers(sparkline["markers_color"]) + + # Write the x14:colorFirst element. + self._write_color_first(sparkline["first_color"]) + + # Write the x14:colorLast element. + self._write_color_last(sparkline["last_color"]) + + # Write the x14:colorHigh element. + self._write_color_high(sparkline["high_color"]) + + # Write the x14:colorLow element. + self._write_color_low(sparkline["low_color"]) + + if sparkline["date_axis"]: + self._xml_data_element("xm:f", sparkline["date_axis"]) + + self._write_sparklines(sparkline) + + self._xml_end_tag("x14:sparklineGroup") + + self._xml_end_tag("x14:sparklineGroups") + self._xml_end_tag("ext") + + def _write_sparklines(self, sparkline): + # Write the <x14:sparklines> element and <x14:sparkline> sub-elements. + + # Write the sparkline elements. + self._xml_start_tag("x14:sparklines") + + for i in range(sparkline["count"]): + spark_range = sparkline["ranges"][i] + location = sparkline["locations"][i] + + self._xml_start_tag("x14:sparkline") + self._xml_data_element("xm:f", spark_range) + self._xml_data_element("xm:sqref", location) + self._xml_end_tag("x14:sparkline") + + self._xml_end_tag("x14:sparklines") + + def _write_ext(self, uri): + # Write the <ext> element. + schema = "http://schemas.microsoft.com/office/" + xmlns_x14 = schema + "spreadsheetml/2009/9/main" + + attributes = [ + ("xmlns:x14", xmlns_x14), + ("uri", uri), + ] + + self._xml_start_tag("ext", attributes) + + def _write_sparkline_groups(self): + # Write the <x14:sparklineGroups> element. + xmlns_xm = "http://schemas.microsoft.com/office/excel/2006/main" + + attributes = [("xmlns:xm", xmlns_xm)] + + self._xml_start_tag("x14:sparklineGroups", attributes) + + def _write_sparkline_group(self, options): + # Write the <x14:sparklineGroup> element. + # + # Example for order. + # + # <x14:sparklineGroup + # manualMax="0" + # manualMin="0" + # lineWeight="2.25" + # type="column" + # dateAxis="1" + # displayEmptyCellsAs="span" + # markers="1" + # high="1" + # low="1" + # first="1" + # last="1" + # negative="1" + # displayXAxis="1" + # displayHidden="1" + # minAxisType="custom" + # maxAxisType="custom" + # rightToLeft="1"> + # + empty = options.get("empty") + attributes = [] + + if options.get("max") is not None: + if options["max"] == "group": + options["cust_max"] = "group" + else: + attributes.append(("manualMax", options["max"])) + options["cust_max"] = "custom" + + if options.get("min") is not None: + if options["min"] == "group": + options["cust_min"] = "group" + else: + attributes.append(("manualMin", options["min"])) + options["cust_min"] = "custom" + + # Ignore the default type attribute (line). + if options["type"] != "line": + attributes.append(("type", options["type"])) + + if options.get("weight"): + attributes.append(("lineWeight", options["weight"])) + + if options.get("date_axis"): + attributes.append(("dateAxis", 1)) + + if empty: + attributes.append(("displayEmptyCellsAs", empty)) + + if options.get("markers"): + attributes.append(("markers", 1)) + + if options.get("high"): + attributes.append(("high", 1)) + + if options.get("low"): + attributes.append(("low", 1)) + + if options.get("first"): + attributes.append(("first", 1)) + + if options.get("last"): + attributes.append(("last", 1)) + + if options.get("negative"): + attributes.append(("negative", 1)) + + if options.get("axis"): + attributes.append(("displayXAxis", 1)) + + if options.get("hidden"): + attributes.append(("displayHidden", 1)) + + if options.get("cust_min"): + attributes.append(("minAxisType", options["cust_min"])) + + if options.get("cust_max"): + attributes.append(("maxAxisType", options["cust_max"])) + + if options.get("reverse"): + attributes.append(("rightToLeft", 1)) + + self._xml_start_tag("x14:sparklineGroup", attributes) + + def _write_spark_color(self, element, color): + # Helper function for the sparkline color functions below. + attributes = [] + + if color.get("rgb"): + attributes.append(("rgb", color["rgb"])) + + if color.get("theme"): + attributes.append(("theme", color["theme"])) + + if color.get("tint"): + attributes.append(("tint", color["tint"])) + + self._xml_empty_tag(element, attributes) + + def _write_color_series(self, color): + # Write the <x14:colorSeries> element. + self._write_spark_color("x14:colorSeries", color) + + def _write_color_negative(self, color): + # Write the <x14:colorNegative> element. + self._write_spark_color("x14:colorNegative", color) + + def _write_color_axis(self): + # Write the <x14:colorAxis> element. + self._write_spark_color("x14:colorAxis", {"rgb": "FF000000"}) + + def _write_color_markers(self, color): + # Write the <x14:colorMarkers> element. + self._write_spark_color("x14:colorMarkers", color) + + def _write_color_first(self, color): + # Write the <x14:colorFirst> element. + self._write_spark_color("x14:colorFirst", color) + + def _write_color_last(self, color): + # Write the <x14:colorLast> element. + self._write_spark_color("x14:colorLast", color) + + def _write_color_high(self, color): + # Write the <x14:colorHigh> element. + self._write_spark_color("x14:colorHigh", color) + + def _write_color_low(self, color): + # Write the <x14:colorLow> element. + self._write_spark_color("x14:colorLow", color) + + def _write_phonetic_pr(self): + # Write the <phoneticPr> element. + attributes = [ + ("fontId", "0"), + ("type", "noConversion"), + ] + + self._xml_empty_tag("phoneticPr", attributes) + + def _write_ignored_errors(self): + # Write the <ignoredErrors> element. + if not self.ignored_errors: + return + + self._xml_start_tag("ignoredErrors") + + if self.ignored_errors.get("number_stored_as_text"): + ignored_range = self.ignored_errors["number_stored_as_text"] + self._write_ignored_error("numberStoredAsText", ignored_range) + + if self.ignored_errors.get("eval_error"): + ignored_range = self.ignored_errors["eval_error"] + self._write_ignored_error("evalError", ignored_range) + + if self.ignored_errors.get("formula_differs"): + ignored_range = self.ignored_errors["formula_differs"] + self._write_ignored_error("formula", ignored_range) + + if self.ignored_errors.get("formula_range"): + ignored_range = self.ignored_errors["formula_range"] + self._write_ignored_error("formulaRange", ignored_range) + + if self.ignored_errors.get("formula_unlocked"): + ignored_range = self.ignored_errors["formula_unlocked"] + self._write_ignored_error("unlockedFormula", ignored_range) + + if self.ignored_errors.get("empty_cell_reference"): + ignored_range = self.ignored_errors["empty_cell_reference"] + self._write_ignored_error("emptyCellReference", ignored_range) + + if self.ignored_errors.get("list_data_validation"): + ignored_range = self.ignored_errors["list_data_validation"] + self._write_ignored_error("listDataValidation", ignored_range) + + if self.ignored_errors.get("calculated_column"): + ignored_range = self.ignored_errors["calculated_column"] + self._write_ignored_error("calculatedColumn", ignored_range) + + if self.ignored_errors.get("two_digit_text_year"): + ignored_range = self.ignored_errors["two_digit_text_year"] + self._write_ignored_error("twoDigitTextYear", ignored_range) + + self._xml_end_tag("ignoredErrors") + + def _write_ignored_error(self, error_type, ignored_range): + # Write the <ignoredError> element. + attributes = [ + ("sqref", ignored_range), + (error_type, 1), + ] + + self._xml_empty_tag("ignoredError", attributes) diff --git a/.venv/lib/python3.12/site-packages/xlsxwriter/xmlwriter.py b/.venv/lib/python3.12/site-packages/xlsxwriter/xmlwriter.py new file mode 100644 index 00000000..60593c90 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlsxwriter/xmlwriter.py @@ -0,0 +1,235 @@ +############################################################################### +# +# XMLwriter - A base class for XlsxWriter classes. +# +# Used in conjunction with XlsxWriter. +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org +# + +# pylint: disable=dangerous-default-value + +# Standard packages. +import re +from io import StringIO + +# Compile performance critical regular expressions. +re_control_chars_1 = re.compile("(_x[0-9a-fA-F]{4}_)") +re_control_chars_2 = re.compile(r"([\x00-\x08\x0b-\x1f])") +xml_escapes = re.compile('["&<>\n]') + + +class XMLwriter: + """ + Simple XML writer class. + + """ + + def __init__(self): + self.fh = None + self.internal_fh = False + + def _set_filehandle(self, filehandle): + # Set the writer filehandle directly. Mainly for testing. + self.fh = filehandle + self.internal_fh = False + + def _set_xml_writer(self, filename): + # Set the XML writer filehandle for the object. + if isinstance(filename, StringIO): + self.internal_fh = False + self.fh = filename + else: + self.internal_fh = True + # pylint: disable-next=consider-using-with + self.fh = open(filename, "w", encoding="utf-8") + + def _xml_close(self): + # Close the XML filehandle if we created it. + if self.internal_fh: + self.fh.close() + + def _xml_declaration(self): + # Write the XML declaration. + self.fh.write('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n') + + def _xml_start_tag(self, tag, attributes=[]): + # Write an XML start tag with optional attributes. + for key, value in attributes: + value = self._escape_attributes(value) + tag += f' {key}="{value}"' + + self.fh.write(f"<{tag}>") + + def _xml_start_tag_unencoded(self, tag, attributes=[]): + # Write an XML start tag with optional, unencoded, attributes. + # This is a minor speed optimization for elements that don't + # need encoding. + for key, value in attributes: + tag += f' {key}="{value}"' + + self.fh.write(f"<{tag}>") + + def _xml_end_tag(self, tag): + # Write an XML end tag. + self.fh.write(f"</{tag}>") + + def _xml_empty_tag(self, tag, attributes=[]): + # Write an empty XML tag with optional attributes. + for key, value in attributes: + value = self._escape_attributes(value) + tag += f' {key}="{value}"' + + self.fh.write(f"<{tag}/>") + + def _xml_empty_tag_unencoded(self, tag, attributes=[]): + # Write an empty XML tag with optional, unencoded, attributes. + # This is a minor speed optimization for elements that don't + # need encoding. + for key, value in attributes: + tag += f' {key}="{value}"' + + self.fh.write(f"<{tag}/>") + + def _xml_data_element(self, tag, data, attributes=[]): + # Write an XML element containing data with optional attributes. + end_tag = tag + + for key, value in attributes: + value = self._escape_attributes(value) + tag += f' {key}="{value}"' + + data = self._escape_data(data) + data = self._escape_control_characters(data) + + self.fh.write(f"<{tag}>{data}</{end_tag}>") + + def _xml_string_element(self, index, attributes=[]): + # Optimized tag writer for <c> cell string elements in the inner loop. + attr = "" + + for key, value in attributes: + value = self._escape_attributes(value) + attr += f' {key}="{value}"' + + self.fh.write(f'<c{attr} t="s"><v>{index}</v></c>') + + def _xml_si_element(self, string, attributes=[]): + # Optimized tag writer for shared strings <si> elements. + attr = "" + + for key, value in attributes: + value = self._escape_attributes(value) + attr += f' {key}="{value}"' + + string = self._escape_data(string) + + self.fh.write(f"<si><t{attr}>{string}</t></si>") + + def _xml_rich_si_element(self, string): + # Optimized tag writer for shared strings <si> rich string elements. + + self.fh.write(f"<si>{string}</si>") + + def _xml_number_element(self, number, attributes=[]): + # Optimized tag writer for <c> cell number elements in the inner loop. + attr = "" + + for key, value in attributes: + value = self._escape_attributes(value) + attr += f' {key}="{value}"' + + self.fh.write(f"<c{attr}><v>{number:.16G}</v></c>") + + def _xml_formula_element(self, formula, result, attributes=[]): + # Optimized tag writer for <c> cell formula elements in the inner loop. + attr = "" + + for key, value in attributes: + value = self._escape_attributes(value) + attr += f' {key}="{value}"' + + formula = self._escape_data(formula) + result = self._escape_data(result) + self.fh.write(f"<c{attr}><f>{formula}</f><v>{result}</v></c>") + + def _xml_inline_string(self, string, preserve, attributes=[]): + # Optimized tag writer for inlineStr cell elements in the inner loop. + attr = "" + t_attr = "" + + # Set the <t> attribute to preserve whitespace. + if preserve: + t_attr = ' xml:space="preserve"' + + for key, value in attributes: + value = self._escape_attributes(value) + attr += f' {key}="{value}"' + + string = self._escape_data(string) + + self.fh.write(f'<c{attr} t="inlineStr"><is><t{t_attr}>{string}</t></is></c>') + + def _xml_rich_inline_string(self, string, attributes=[]): + # Optimized tag writer for rich inlineStr in the inner loop. + attr = "" + + for key, value in attributes: + value = self._escape_attributes(value) + attr += f' {key}="{value}"' + + self.fh.write(f'<c{attr} t="inlineStr"><is>{string}</is></c>') + + def _escape_attributes(self, attribute): + # Escape XML characters in attributes. + try: + if not xml_escapes.search(attribute): + return attribute + except TypeError: + return attribute + + attribute = ( + attribute.replace("&", "&") + .replace('"', """) + .replace("<", "<") + .replace(">", ">") + .replace("\n", "
") + ) + return attribute + + def _escape_data(self, data): + # Escape XML characters in data sections of tags. Note, this + # is different from _escape_attributes() in that double quotes + # are not escaped by Excel. + try: + if not xml_escapes.search(data): + return data + except TypeError: + return data + + data = data.replace("&", "&").replace("<", "<").replace(">", ">") + return data + + @staticmethod + def _escape_control_characters(data): + # Excel escapes control characters with _xHHHH_ and also escapes any + # literal strings of that type by encoding the leading underscore. + # So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_. + # The following substitutions deal with those cases. + try: + # Escape the escape. + data = re_control_chars_1.sub(r"_x005F\1", data) + except TypeError: + return data + + # Convert control character to the _xHHHH_ escape. + data = re_control_chars_2.sub( + lambda match: f"_x{ord(match.group(1)):04X}_", data + ) + + # Escapes non characters in strings. + data = data.replace("\uFFFE", "_xFFFE_").replace("\uFFFF", "_xFFFF_") + + return data |