about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/xlsxwriter/workbook.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/xlsxwriter/workbook.py')
-rw-r--r--.venv/lib/python3.12/site-packages/xlsxwriter/workbook.py1856
1 files changed, 1856 insertions, 0 deletions
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