aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/xlsxwriter
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/xlsxwriter
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/xlsxwriter')
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/__init__.py8
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/app.py199
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/chart.py4382
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/chart_area.py102
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/chart_bar.py176
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/chart_column.py133
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/chart_doughnut.py101
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/chart_line.py144
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/chart_pie.py263
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/chart_radar.py103
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/chart_scatter.py336
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/chart_stock.py125
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/chartsheet.py197
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/comments.py212
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/contenttypes.py269
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/core.py206
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/custom.py142
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/drawing.py1196
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/exceptions.py56
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/feature_property_bag.py156
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/format.py1217
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/metadata.py266
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/packager.py880
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/relationships.py143
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/rich_value.py97
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/rich_value_rel.py82
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/rich_value_structure.py99
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/rich_value_types.py111
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/shape.py416
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/sharedstrings.py138
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/styles.py803
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/table.py194
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/theme.py69
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/utility.py1207
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/vml.py707
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/workbook.py1856
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/worksheet.py8554
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/xmlwriter.py235
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("&", "&amp;")
+ .replace('"', "&quot;")
+ .replace("<", "&lt;")
+ .replace(">", "&gt;")
+ .replace("\n", "&#xA;")
+ )
+ 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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
+ 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