Add xlsxwriter-based Excel generation scripts with openpyxl implementation
- Created create_excel_xlsxwriter.py and update_excel_xlsxwriter.py - Uses openpyxl exclusively to preserve Excel formatting and formulas - Updated server.js to use new xlsxwriter scripts for form submissions - Maintains all original functionality while ensuring proper Excel file handling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
10
venv/lib/python3.12/site-packages/xlsxwriter/__init__.py
Normal file
10
venv/lib/python3.12/site-packages/xlsxwriter/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
#
|
||||
# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
|
||||
#
|
||||
__version__ = "3.2.9"
|
||||
__VERSION__ = __version__
|
||||
from .workbook import Workbook # noqa
|
||||
|
||||
__all__ = ["Workbook"]
|
||||
202
venv/lib/python3.12/site-packages/xlsxwriter/app.py
Normal file
202
venv/lib/python3.12/site-packages/xlsxwriter/app.py
Normal file
@@ -0,0 +1,202 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from . import xmlwriter
|
||||
|
||||
|
||||
class App(xmlwriter.XMLwriter):
|
||||
"""
|
||||
A class for writing the Excel XLSX App file.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Public API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.part_names = []
|
||||
self.heading_pairs = []
|
||||
self.properties = {}
|
||||
self.doc_security = 0
|
||||
|
||||
def _add_part_name(self, part_name: str) -> None:
|
||||
# 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: Tuple[str, int]) -> None:
|
||||
# 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: Dict[str, str]) -> None:
|
||||
# Set the document properties.
|
||||
self.properties = properties
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Private API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _assemble_xml_file(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <Application> element.
|
||||
self._xml_data_element("Application", "Microsoft Excel")
|
||||
|
||||
def _write_doc_security(self) -> None:
|
||||
# Write the <DocSecurity> element.
|
||||
self._xml_data_element("DocSecurity", self.doc_security)
|
||||
|
||||
def _write_scale_crop(self) -> None:
|
||||
# Write the <ScaleCrop> element.
|
||||
self._xml_data_element("ScaleCrop", "false")
|
||||
|
||||
def _write_heading_pairs(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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: str, vector_data: List[Tuple[str, int]]
|
||||
) -> None:
|
||||
# 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: Tuple[str, int]) -> None:
|
||||
# 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) -> None:
|
||||
company = self.properties.get("company", "")
|
||||
|
||||
self._xml_data_element("Company", company)
|
||||
|
||||
def _write_manager(self) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <LinksUpToDate> element.
|
||||
self._xml_data_element("LinksUpToDate", "false")
|
||||
|
||||
def _write_shared_doc(self) -> None:
|
||||
# Write the <SharedDoc> element.
|
||||
self._xml_data_element("SharedDoc", "false")
|
||||
|
||||
def _write_hyperlink_base(self) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <HyperlinksChanged> element.
|
||||
self._xml_data_element("HyperlinksChanged", "false")
|
||||
|
||||
def _write_app_version(self) -> None:
|
||||
# Write the <AppVersion> element.
|
||||
self._xml_data_element("AppVersion", "12.0000")
|
||||
4387
venv/lib/python3.12/site-packages/xlsxwriter/chart.py
Normal file
4387
venv/lib/python3.12/site-packages/xlsxwriter/chart.py
Normal file
File diff suppressed because it is too large
Load Diff
104
venv/lib/python3.12/site-packages/xlsxwriter/chart_area.py
Normal file
104
venv/lib/python3.12/site-packages/xlsxwriter/chart_area.py
Normal file
@@ -0,0 +1,104 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 typing import Any, Dict, Optional
|
||||
|
||||
from . import chart
|
||||
|
||||
|
||||
class ChartArea(chart.Chart):
|
||||
"""
|
||||
A class for writing the Excel XLSX Area charts.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Public API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def __init__(self, options: Optional[Dict[str, Any]] = None) -> 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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")
|
||||
177
venv/lib/python3.12/site-packages/xlsxwriter/chart_bar.py
Normal file
177
venv/lib/python3.12/site-packages/xlsxwriter/chart_bar.py
Normal file
@@ -0,0 +1,177 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 typing import Any, Dict, Optional
|
||||
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: Optional[Dict[str, Any]] = None) -> 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: Optional[chart.Chart] = None) -> 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <c:barDir> element.
|
||||
val = "bar"
|
||||
|
||||
attributes = [("val", val)]
|
||||
|
||||
self._xml_empty_tag("c:barDir", attributes)
|
||||
|
||||
def _write_err_dir(self, val) -> None:
|
||||
# Overridden from Chart class since it is not used in Bar charts.
|
||||
pass
|
||||
135
venv/lib/python3.12/site-packages/xlsxwriter/chart_column.py
Normal file
135
venv/lib/python3.12/site-packages/xlsxwriter/chart_column.py
Normal file
@@ -0,0 +1,135 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 typing import Any, Dict, Optional
|
||||
|
||||
from . import chart
|
||||
|
||||
|
||||
class ChartColumn(chart.Chart):
|
||||
"""
|
||||
A class for writing the Excel XLSX Column charts.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Public API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def __init__(self, options: Optional[Dict[str, Any]] = None) -> 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <c:barDir> element.
|
||||
val = "col"
|
||||
|
||||
attributes = [("val", val)]
|
||||
|
||||
self._xml_empty_tag("c:barDir", attributes)
|
||||
|
||||
def _write_err_dir(self, val) -> None:
|
||||
# Overridden from Chart class since it is not used in Column charts.
|
||||
pass
|
||||
101
venv/lib/python3.12/site-packages/xlsxwriter/chart_doughnut.py
Normal file
101
venv/lib/python3.12/site-packages/xlsxwriter/chart_doughnut.py
Normal file
@@ -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) -> None:
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.vary_data_color = 1
|
||||
self.rotation = 0
|
||||
self.hole_size = 50
|
||||
|
||||
def set_hole_size(self, size: int) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <c:holeSize> element.
|
||||
attributes = [("val", self.hole_size)]
|
||||
|
||||
self._xml_empty_tag("c:holeSize", attributes)
|
||||
146
venv/lib/python3.12/site-packages/xlsxwriter/chart_line.py
Normal file
146
venv/lib/python3.12/site-packages/xlsxwriter/chart_line.py
Normal file
@@ -0,0 +1,146 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 typing import Any, Dict, Optional
|
||||
|
||||
from . import chart
|
||||
|
||||
|
||||
class ChartLine(chart.Chart):
|
||||
"""
|
||||
A class for writing the Excel XLSX Line charts.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Public API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def __init__(self, options: Optional[Dict[str, Any]] = None) -> 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <c:marker> element without a sub-element.
|
||||
attributes = [("val", 1)]
|
||||
|
||||
self._xml_empty_tag("c:marker", attributes)
|
||||
263
venv/lib/python3.12/site-packages/xlsxwriter/chart_pie.py
Normal file
263
venv/lib/python3.12/site-packages/xlsxwriter/chart_pie.py
Normal file
@@ -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) -> None:
|
||||
"""
|
||||
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: int) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <c:varyColors> element.
|
||||
attributes = [("val", 1)]
|
||||
|
||||
self._xml_empty_tag("c:varyColors", attributes)
|
||||
|
||||
def _write_first_slice_ang(self) -> None:
|
||||
# Write the <c:firstSliceAng> element.
|
||||
attributes = [("val", self.rotation)]
|
||||
|
||||
self._xml_empty_tag("c:firstSliceAng", attributes)
|
||||
|
||||
def _write_show_leader_lines(self) -> None:
|
||||
# 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)
|
||||
105
venv/lib/python3.12/site-packages/xlsxwriter/chart_radar.py
Normal file
105
venv/lib/python3.12/site-packages/xlsxwriter/chart_radar.py
Normal file
@@ -0,0 +1,105 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 typing import Any, Dict, Optional
|
||||
|
||||
from . import chart
|
||||
|
||||
|
||||
class ChartRadar(chart.Chart):
|
||||
"""
|
||||
A class for writing the Excel XLSX Radar charts.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Public API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def __init__(self, options: Optional[Dict[str, Any]] = None) -> 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) -> None:
|
||||
# Write the c:radarChart element.
|
||||
self._write_radar_chart(args)
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# XML methods.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _write_radar_chart(self, args) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <c:radarStyle> element.
|
||||
val = "marker"
|
||||
|
||||
if self.subtype == "filled":
|
||||
val = "filled"
|
||||
|
||||
attributes = [("val", val)]
|
||||
|
||||
self._xml_empty_tag("c:radarStyle", attributes)
|
||||
337
venv/lib/python3.12/site-packages/xlsxwriter/chart_scatter.py
Normal file
337
venv/lib/python3.12/site-packages/xlsxwriter/chart_scatter.py
Normal file
@@ -0,0 +1,337 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 typing import Any, Dict, Optional
|
||||
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: Optional[Dict[str, Any]] = None) -> 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: Optional[chart.Chart] = None) -> 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <c:scatterStyle> element.
|
||||
attributes = [("val", val)]
|
||||
|
||||
self._xml_empty_tag("c:scatterStyle", attributes)
|
||||
|
||||
def _modify_series_formatting(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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")
|
||||
125
venv/lib/python3.12/site-packages/xlsxwriter/chart_stock.py
Normal file
125
venv/lib/python3.12/site-packages/xlsxwriter/chart_stock.py
Normal file
@@ -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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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
|
||||
110
venv/lib/python3.12/site-packages/xlsxwriter/chart_title.py
Normal file
110
venv/lib/python3.12/site-packages/xlsxwriter/chart_title.py
Normal file
@@ -0,0 +1,110 @@
|
||||
###############################################################################
|
||||
#
|
||||
# ChartTitle - A class for representing Excel chart titles.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
#
|
||||
# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
|
||||
#
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class ChartTitle:
|
||||
"""
|
||||
A class to represent an Excel chart title.
|
||||
|
||||
This class encapsulates all title related properties and methods for the
|
||||
chart title and axis titles.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Initialize a ChartTitle instance.
|
||||
"""
|
||||
self.font: Optional[Dict[str, Any]] = None
|
||||
self.name: Optional[str] = None
|
||||
self.formula: Optional[str] = None
|
||||
self.data_id: Optional[int] = None
|
||||
self.layout: Optional[Dict[str, Any]] = None
|
||||
self.overlay: Optional[bool] = None
|
||||
self.hidden: bool = False
|
||||
self.line: Optional[Dict[str, Any]] = None
|
||||
self.fill: Optional[Dict[str, Any]] = None
|
||||
self.pattern: Optional[Dict[str, Any]] = None
|
||||
self.gradient: Optional[Dict[str, Any]] = None
|
||||
|
||||
def has_name(self) -> bool:
|
||||
"""
|
||||
Check if the title has a text name set.
|
||||
|
||||
Returns:
|
||||
True if name has been set.
|
||||
"""
|
||||
return self.name is not None and self.name != ""
|
||||
|
||||
def has_formula(self) -> bool:
|
||||
"""
|
||||
Check if the title has a formula set.
|
||||
|
||||
Returns:
|
||||
True if formula has been set.
|
||||
"""
|
||||
return self.formula is not None
|
||||
|
||||
def has_formatting(self) -> bool:
|
||||
"""
|
||||
Check if the title has any formatting properties set.
|
||||
|
||||
Returns:
|
||||
True if the title has line, fill, pattern, or gradient formatting.
|
||||
"""
|
||||
has_line = self.line is not None and self.line.get("defined", False)
|
||||
has_fill = self.fill is not None and self.fill.get("defined", False)
|
||||
has_pattern = self.pattern
|
||||
has_gradient = self.gradient
|
||||
|
||||
return has_line or has_fill or has_pattern or has_gradient
|
||||
|
||||
def get_formatting(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get a dictionary containing the formatting properties.
|
||||
|
||||
Returns:
|
||||
A dictionary with line, fill, pattern, and gradient properties.
|
||||
"""
|
||||
return {
|
||||
"line": self.line,
|
||||
"fill": self.fill,
|
||||
"pattern": self.pattern,
|
||||
"gradient": self.gradient,
|
||||
}
|
||||
|
||||
def is_hidden(self) -> bool:
|
||||
"""
|
||||
Check if the title is explicitly hidden.
|
||||
|
||||
Returns:
|
||||
True if title is hidden.
|
||||
"""
|
||||
return self.hidden
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the ChartTitle.
|
||||
"""
|
||||
return (
|
||||
f"ChartTitle(\n"
|
||||
f" name = {self.name!r},\n"
|
||||
f" formula = {self.formula!r},\n"
|
||||
f" hidden = {self.hidden!r},\n"
|
||||
f" font = {self.font!r},\n"
|
||||
f" line = {self.line!r},\n"
|
||||
f" fill = {self.fill!r},\n"
|
||||
f" pattern = {self.pattern!r},\n"
|
||||
f" gradient = {self.gradient!r},\n"
|
||||
f" layout = {self.layout!r},\n"
|
||||
f" overlay = {self.overlay!r},\n"
|
||||
f" has_formatting = {self.has_formatting()!r},\n"
|
||||
f")\n"
|
||||
)
|
||||
203
venv/lib/python3.12/site-packages/xlsxwriter/chartsheet.py
Normal file
203
venv/lib/python3.12/site-packages/xlsxwriter/chartsheet.py
Normal file
@@ -0,0 +1,203 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 typing import Any, Dict, Optional
|
||||
|
||||
from xlsxwriter.chart import Chart
|
||||
|
||||
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) -> None:
|
||||
"""
|
||||
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: Chart) -> 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: str = "", options: Optional[Dict[str, Any]] = None
|
||||
) -> 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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)
|
||||
431
venv/lib/python3.12/site-packages/xlsxwriter/color.py
Normal file
431
venv/lib/python3.12/site-packages/xlsxwriter/color.py
Normal file
@@ -0,0 +1,431 @@
|
||||
###############################################################################
|
||||
#
|
||||
# Color - A class to represent Excel colors.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
#
|
||||
# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
|
||||
#
|
||||
|
||||
|
||||
from enum import Enum
|
||||
from typing import List, Tuple, Union
|
||||
|
||||
CHART_THEMES = [
|
||||
# Color 0 (bg1).
|
||||
[
|
||||
("bg1", 0, 0),
|
||||
("bg1", 95000, 0),
|
||||
("bg1", 85000, 0),
|
||||
("bg1", 75000, 0),
|
||||
("bg1", 65000, 0),
|
||||
("bg1", 50000, 0),
|
||||
],
|
||||
# Color 1 (tx1).
|
||||
[
|
||||
("tx1", 0, 0),
|
||||
("tx1", 50000, 50000),
|
||||
("tx1", 65000, 35000),
|
||||
("tx1", 75000, 25000),
|
||||
("tx1", 85000, 15000),
|
||||
("tx1", 95000, 5000),
|
||||
],
|
||||
# Color 2 (bg2).
|
||||
[
|
||||
("bg2", 0, 0),
|
||||
("bg2", 90000, 0),
|
||||
("bg2", 75000, 0),
|
||||
("bg2", 50000, 0),
|
||||
("bg2", 25000, 0),
|
||||
("bg2", 10000, 0),
|
||||
],
|
||||
# Color 3 (tx2).
|
||||
[
|
||||
("tx2", 0, 0),
|
||||
("tx2", 20000, 80000),
|
||||
("tx2", 40000, 60000),
|
||||
("tx2", 60000, 40000),
|
||||
("tx2", 75000, 0),
|
||||
("tx2", 50000, 0),
|
||||
],
|
||||
# Color 4 (accent1).
|
||||
[
|
||||
("accent1", 0, 0),
|
||||
("accent1", 20000, 80000),
|
||||
("accent1", 40000, 60000),
|
||||
("accent1", 60000, 40000),
|
||||
("accent1", 75000, 0),
|
||||
("accent1", 50000, 0),
|
||||
],
|
||||
# Color 5 (accent2).
|
||||
[
|
||||
("accent2", 0, 0),
|
||||
("accent2", 20000, 80000),
|
||||
("accent2", 40000, 60000),
|
||||
("accent2", 60000, 40000),
|
||||
("accent2", 75000, 0),
|
||||
("accent2", 50000, 0),
|
||||
],
|
||||
# Color 6 (accent3).
|
||||
[
|
||||
("accent3", 0, 0),
|
||||
("accent3", 20000, 80000),
|
||||
("accent3", 40000, 60000),
|
||||
("accent3", 60000, 40000),
|
||||
("accent3", 75000, 0),
|
||||
("accent3", 50000, 0),
|
||||
],
|
||||
# Color 7 (accent4).
|
||||
[
|
||||
("accent4", 0, 0),
|
||||
("accent4", 20000, 80000),
|
||||
("accent4", 40000, 60000),
|
||||
("accent4", 60000, 40000),
|
||||
("accent4", 75000, 0),
|
||||
("accent4", 50000, 0),
|
||||
],
|
||||
# Color 8 (accent5).
|
||||
[
|
||||
("accent5", 0, 0),
|
||||
("accent5", 20000, 80000),
|
||||
("accent5", 40000, 60000),
|
||||
("accent5", 60000, 40000),
|
||||
("accent5", 75000, 0),
|
||||
("accent5", 50000, 0),
|
||||
],
|
||||
# Color 9 (accent6).
|
||||
[
|
||||
("accent6", 0, 0),
|
||||
("accent6", 20000, 80000),
|
||||
("accent6", 40000, 60000),
|
||||
("accent6", 60000, 40000),
|
||||
("accent6", 75000, 0),
|
||||
("accent6", 50000, 0),
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
class ColorTypes(Enum):
|
||||
"""
|
||||
Enum to represent different types of URLS.
|
||||
"""
|
||||
|
||||
RGB = 1
|
||||
THEME = 2
|
||||
|
||||
|
||||
class Color:
|
||||
"""
|
||||
A class to represent an Excel color.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, color: Union[str, int, Tuple[int, int]]) -> None:
|
||||
"""
|
||||
Initialize a Color instance.
|
||||
|
||||
Args:
|
||||
color (Union[str, int, Tuple[int, int]]): The value of the color
|
||||
(e.g., a hex string, an integer, or a tuple of two integers).
|
||||
"""
|
||||
self._rgb_value: int = 0x000000
|
||||
self._type: ColorTypes = ColorTypes.RGB
|
||||
self._theme_color: Tuple[int, int] = (0, 0)
|
||||
self._is_automatic: bool = False
|
||||
|
||||
if isinstance(color, str):
|
||||
self._parse_string_color(color)
|
||||
self._type = ColorTypes.RGB
|
||||
elif isinstance(color, int):
|
||||
if color > 0xFFFFFF:
|
||||
raise ValueError("RGB color must be in the range 0x000000 - 0xFFFFFF.")
|
||||
|
||||
self._rgb_value = color
|
||||
self._type = ColorTypes.RGB
|
||||
elif (
|
||||
isinstance(color, tuple)
|
||||
and len(color) == 2
|
||||
and all(isinstance(v, int) for v in color)
|
||||
):
|
||||
if color[0] > 9:
|
||||
raise ValueError("Theme color must be in the range 0-9.")
|
||||
if color[1] > 5:
|
||||
raise ValueError("Theme shade must be in the range 0-5.")
|
||||
|
||||
self._theme_color = color
|
||||
self._type = ColorTypes.THEME
|
||||
else:
|
||||
raise ValueError(
|
||||
"Invalid color value. Must be a string, integer, or tuple."
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the Color instance.
|
||||
"""
|
||||
if self._type == ColorTypes.RGB:
|
||||
value = f"0x{self._rgb_value:06X}"
|
||||
else:
|
||||
value = f"Theme({self._theme_color[0]}, {self._theme_color[1]})"
|
||||
|
||||
return (
|
||||
f"Color("
|
||||
f"value={value}, "
|
||||
f"type={self._type.name}, "
|
||||
f"is_automatic={self._is_automatic})"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _from_value(value: Union["Color", str]) -> "Color":
|
||||
"""
|
||||
Internal method to convert a string to a Color instance or return the
|
||||
Color instance if already provided. This is mainly used for backward
|
||||
compatibility support in the XlsxWriter API.
|
||||
|
||||
Args:
|
||||
value (Union[Color, str]): A Color instance or a string representing
|
||||
a color.
|
||||
|
||||
Returns:
|
||||
Color: A Color instance.
|
||||
"""
|
||||
if isinstance(value, Color):
|
||||
return value
|
||||
|
||||
if isinstance(value, str):
|
||||
return Color(value)
|
||||
|
||||
raise TypeError("Value must be a Color instance or a string.")
|
||||
|
||||
@staticmethod
|
||||
def rgb(color: str) -> "Color":
|
||||
"""
|
||||
Create a user-defined RGB color from a Html color string.
|
||||
|
||||
Args:
|
||||
color (int): An RGB value in the range 0x000000 (black) to 0xFFFFFF (white).
|
||||
|
||||
Returns:
|
||||
Color: A Color object representing an Excel RGB color.
|
||||
"""
|
||||
return Color(color)
|
||||
|
||||
@staticmethod
|
||||
def rgb_integer(color: int) -> "Color":
|
||||
"""
|
||||
Create a user-defined RGB color from an integer value.
|
||||
|
||||
Args:
|
||||
color (int): An RGB value in the range 0x000000 (black) to 0xFFFFFF (white).
|
||||
|
||||
Returns:
|
||||
Color: A Color object representing an Excel RGB color.
|
||||
"""
|
||||
if color > 0xFFFFFF:
|
||||
raise ValueError("RGB color must be in the range 0x000000 - 0xFFFFFF.")
|
||||
return Color(color)
|
||||
|
||||
@staticmethod
|
||||
def theme(color: int, shade: int) -> "Color":
|
||||
"""
|
||||
Create a theme color.
|
||||
|
||||
Args:
|
||||
color (int): The theme color index (0-9).
|
||||
shade (int): The theme shade index (0-5).
|
||||
|
||||
Returns:
|
||||
Color: A Color object representing an Excel Theme color.
|
||||
"""
|
||||
if color > 9:
|
||||
raise ValueError("Theme color must be in the range 0-9.")
|
||||
if shade > 5:
|
||||
raise ValueError("Theme shade must be in the range 0-5.")
|
||||
return Color((color, shade))
|
||||
|
||||
@staticmethod
|
||||
def automatic() -> "Color":
|
||||
"""
|
||||
Create an Excel color representing an "Automatic" color.
|
||||
|
||||
The Automatic color for an Excel property is usually the same as the
|
||||
Default color but can vary according to system settings. This method and
|
||||
color type are rarely used in practice but are included for completeness.
|
||||
|
||||
Returns:
|
||||
Color: A Color object representing an Excel Automatic color.
|
||||
"""
|
||||
color = Color(0x000000)
|
||||
color._is_automatic = True
|
||||
|
||||
return color
|
||||
|
||||
def _parse_string_color(self, value: str) -> None:
|
||||
"""
|
||||
Convert a hex string or named color to an RGB value.
|
||||
|
||||
Returns:
|
||||
int: The RGB value.
|
||||
"""
|
||||
# Named colors used in conjunction with various set_xxx_color methods to
|
||||
# convert a color name into an RGB value. These colors are for backward
|
||||
# compatibility with older versions of Excel.
|
||||
named_colors = {
|
||||
"red": 0xFF0000,
|
||||
"blue": 0x0000FF,
|
||||
"cyan": 0x00FFFF,
|
||||
"gray": 0x808080,
|
||||
"lime": 0x00FF00,
|
||||
"navy": 0x000080,
|
||||
"pink": 0xFF00FF,
|
||||
"black": 0x000000,
|
||||
"brown": 0x800000,
|
||||
"green": 0x008000,
|
||||
"white": 0xFFFFFF,
|
||||
"orange": 0xFF6600,
|
||||
"purple": 0x800080,
|
||||
"silver": 0xC0C0C0,
|
||||
"yellow": 0xFFFF00,
|
||||
"magenta": 0xFF00FF,
|
||||
}
|
||||
|
||||
color = value.lstrip("#").lower()
|
||||
|
||||
if color == "automatic":
|
||||
self._is_automatic = True
|
||||
self._rgb_value = 0x000000
|
||||
elif color in named_colors:
|
||||
self._rgb_value = named_colors[color]
|
||||
else:
|
||||
try:
|
||||
self._rgb_value = int(color, 16)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid color value: {value}") from e
|
||||
|
||||
def _rgb_hex_value(self) -> str:
|
||||
"""
|
||||
Get the RGB hex value for the color.
|
||||
|
||||
Returns:
|
||||
str: The RGB hex value as a string.
|
||||
"""
|
||||
if self._is_automatic:
|
||||
# Default to black for automatic colors.
|
||||
return "000000"
|
||||
|
||||
if self._type == ColorTypes.THEME:
|
||||
# Default to black for theme colors.
|
||||
return "000000"
|
||||
|
||||
return f"{self._rgb_value:06X}"
|
||||
|
||||
def _vml_rgb_hex_value(self) -> str:
|
||||
"""
|
||||
Get the RGB hex value for a VML fill color in "#rrggbb" format.
|
||||
|
||||
Returns:
|
||||
str: The RGB hex value as a string.
|
||||
"""
|
||||
if self._is_automatic:
|
||||
# Default VML color for non-RGB colors.
|
||||
return "#ffffe1"
|
||||
|
||||
return f"#{self._rgb_hex_value().lower()}"
|
||||
|
||||
def _argb_hex_value(self) -> str:
|
||||
"""
|
||||
Get the ARGB hex value for the color. The alpha channel is always FF.
|
||||
|
||||
Returns:
|
||||
str: The ARGB hex value as a string.
|
||||
"""
|
||||
return f"FF{self._rgb_hex_value()}"
|
||||
|
||||
def _attributes(self) -> List[Tuple[str, str]]:
|
||||
"""
|
||||
Convert the color into a set of "rgb" or "theme/tint" attributes used in
|
||||
color-related Style XML elements.
|
||||
|
||||
Returns:
|
||||
list[tuple[str, str]]: A list of key-value pairs representing the
|
||||
attributes.
|
||||
"""
|
||||
# pylint: disable=too-many-return-statements
|
||||
# pylint: disable=no-else-return
|
||||
if self._type == ColorTypes.THEME:
|
||||
color, shade = self._theme_color
|
||||
|
||||
# The first 3 columns of colors in the theme palette are different
|
||||
# from the others.
|
||||
if color == 0:
|
||||
if shade == 1:
|
||||
return [("theme", str(color)), ("tint", "-4.9989318521683403E-2")]
|
||||
elif shade == 2:
|
||||
return [("theme", str(color)), ("tint", "-0.14999847407452621")]
|
||||
elif shade == 3:
|
||||
return [("theme", str(color)), ("tint", "-0.249977111117893")]
|
||||
elif shade == 4:
|
||||
return [("theme", str(color)), ("tint", "-0.34998626667073579")]
|
||||
elif shade == 5:
|
||||
return [("theme", str(color)), ("tint", "-0.499984740745262")]
|
||||
else:
|
||||
return [("theme", str(color))]
|
||||
|
||||
elif color == 1:
|
||||
if shade == 1:
|
||||
return [("theme", str(color)), ("tint", "0.499984740745262")]
|
||||
elif shade == 2:
|
||||
return [("theme", str(color)), ("tint", "0.34998626667073579")]
|
||||
elif shade == 3:
|
||||
return [("theme", str(color)), ("tint", "0.249977111117893")]
|
||||
elif shade == 4:
|
||||
return [("theme", str(color)), ("tint", "0.14999847407452621")]
|
||||
elif shade == 5:
|
||||
return [("theme", str(color)), ("tint", "4.9989318521683403E-2")]
|
||||
else:
|
||||
return [("theme", str(color))]
|
||||
|
||||
elif color == 2:
|
||||
if shade == 1:
|
||||
return [("theme", str(color)), ("tint", "-9.9978637043366805E-2")]
|
||||
elif shade == 2:
|
||||
return [("theme", str(color)), ("tint", "-0.249977111117893")]
|
||||
elif shade == 3:
|
||||
return [("theme", str(color)), ("tint", "-0.499984740745262")]
|
||||
elif shade == 4:
|
||||
return [("theme", str(color)), ("tint", "-0.749992370372631")]
|
||||
elif shade == 5:
|
||||
return [("theme", str(color)), ("tint", "-0.89999084444715716")]
|
||||
else:
|
||||
return [("theme", str(color))]
|
||||
|
||||
else:
|
||||
if shade == 1:
|
||||
return [("theme", str(color)), ("tint", "0.79998168889431442")]
|
||||
elif shade == 2:
|
||||
return [("theme", str(color)), ("tint", "0.59999389629810485")]
|
||||
elif shade == 3:
|
||||
return [("theme", str(color)), ("tint", "0.39997558519241921")]
|
||||
elif shade == 4:
|
||||
return [("theme", str(color)), ("tint", "-0.249977111117893")]
|
||||
elif shade == 5:
|
||||
return [("theme", str(color)), ("tint", "-0.499984740745262")]
|
||||
else:
|
||||
return [("theme", str(color))]
|
||||
|
||||
# Handle RGB color.
|
||||
elif self._type == ColorTypes.RGB:
|
||||
return [("rgb", self._argb_hex_value())]
|
||||
|
||||
# Default case for other colors.
|
||||
return []
|
||||
|
||||
def _chart_scheme(self) -> Tuple[str, int, int]:
|
||||
"""
|
||||
Return the chart theme based on color and shade.
|
||||
|
||||
Returns:
|
||||
Tuple[str, int, int]: The corresponding tuple of values from CHART_THEMES.
|
||||
|
||||
"""
|
||||
return CHART_THEMES[self._theme_color[0]][self._theme_color[1]]
|
||||
392
venv/lib/python3.12/site-packages/xlsxwriter/comments.py
Normal file
392
venv/lib/python3.12/site-packages/xlsxwriter/comments.py
Normal file
@@ -0,0 +1,392 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 typing import Dict, List, Optional, Union
|
||||
|
||||
from xlsxwriter.color import Color
|
||||
|
||||
from . import xmlwriter
|
||||
from .utility import _preserve_whitespace, xl_cell_to_rowcol, xl_rowcol_to_cell
|
||||
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# A comment type class.
|
||||
#
|
||||
###########################################################################
|
||||
class CommentType:
|
||||
"""
|
||||
A class to represent a comment in an Excel worksheet.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
row: int,
|
||||
col: int,
|
||||
text: str,
|
||||
options: Optional[Dict[str, Union[str, int, float]]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize a Comment instance.
|
||||
|
||||
Args:
|
||||
row (int): The row number of the comment.
|
||||
col (int): The column number of the comment.
|
||||
text (str): The text of the comment.
|
||||
options (dict): Additional options for the comment.
|
||||
"""
|
||||
self.row: int = row
|
||||
self.col: int = col
|
||||
self.text: str = text
|
||||
|
||||
self.author: Optional[str] = None
|
||||
self.color: Color = Color("#ffffe1")
|
||||
|
||||
self.start_row: int = 0
|
||||
self.start_col: int = 0
|
||||
|
||||
self.is_visible: Optional[bool] = None
|
||||
|
||||
self.width: float = 128
|
||||
self.height: float = 74
|
||||
|
||||
self.x_scale: float = 1
|
||||
self.y_scale: float = 1
|
||||
self.x_offset: int = 0
|
||||
self.y_offset: int = 0
|
||||
|
||||
self.font_size: float = 8
|
||||
self.font_name: str = "Tahoma"
|
||||
self.font_family: int = 2
|
||||
|
||||
self.vertices: List[Union[int, float]] = []
|
||||
|
||||
# Set the default start cell and offsets for the comment.
|
||||
self.set_offsets(self.row, self.col)
|
||||
|
||||
# Set any user supplied options.
|
||||
self._set_user_options(options)
|
||||
|
||||
def _set_user_options(
|
||||
self, options: Optional[Dict[str, Union[str, int, float]]] = None
|
||||
) -> None:
|
||||
"""
|
||||
This method handles the additional optional parameters to
|
||||
``write_comment()``.
|
||||
"""
|
||||
if options is None:
|
||||
return
|
||||
|
||||
# Overwrite the defaults with any user supplied values. Incorrect or
|
||||
# misspelled parameters are silently ignored.
|
||||
width = options.get("width")
|
||||
if width and isinstance(width, (int, float)):
|
||||
self.width = width
|
||||
|
||||
height = options.get("height")
|
||||
if height and isinstance(height, (int, float)):
|
||||
self.height = height
|
||||
|
||||
x_offset = options.get("x_offset")
|
||||
if x_offset and isinstance(x_offset, int):
|
||||
self.x_offset = x_offset
|
||||
|
||||
y_offset = options.get("y_offset")
|
||||
if y_offset and isinstance(y_offset, int):
|
||||
self.y_offset = y_offset
|
||||
|
||||
start_col = options.get("start_col")
|
||||
if start_col and isinstance(start_col, int):
|
||||
self.start_col = start_col
|
||||
|
||||
start_row = options.get("start_row")
|
||||
if start_row and isinstance(start_row, int):
|
||||
self.start_row = start_row
|
||||
|
||||
font_size = options.get("font_size")
|
||||
if font_size and isinstance(font_size, (int, float)):
|
||||
self.font_size = font_size
|
||||
|
||||
font_name = options.get("font_name")
|
||||
if font_name and isinstance(font_name, str):
|
||||
self.font_name = font_name
|
||||
|
||||
font_family = options.get("font_family")
|
||||
if font_family and isinstance(font_family, int):
|
||||
self.font_family = font_family
|
||||
|
||||
author = options.get("author")
|
||||
if author and isinstance(author, str):
|
||||
self.author = author
|
||||
|
||||
visible = options.get("visible")
|
||||
if visible is not None and isinstance(visible, bool):
|
||||
self.is_visible = visible
|
||||
|
||||
if options.get("color"):
|
||||
# Set the comment background color.
|
||||
self.color = Color._from_value(options["color"])
|
||||
|
||||
# Convert a cell reference to a row and column.
|
||||
start_cell = options.get("start_cell")
|
||||
if start_cell and isinstance(start_cell, str):
|
||||
(start_row, start_col) = xl_cell_to_rowcol(start_cell)
|
||||
self.start_row = start_row
|
||||
self.start_col = start_col
|
||||
|
||||
# Scale the size of the comment box if required.
|
||||
x_scale = options.get("x_scale")
|
||||
if x_scale and isinstance(x_scale, (int, float)):
|
||||
self.width = self.width * x_scale
|
||||
|
||||
y_scale = options.get("y_scale")
|
||||
if y_scale and isinstance(y_scale, (int, float)):
|
||||
self.height = self.height * y_scale
|
||||
|
||||
# Round the dimensions to the nearest pixel.
|
||||
self.width = int(0.5 + self.width)
|
||||
self.height = int(0.5 + self.height)
|
||||
|
||||
def set_offsets(self, row: int, col: int) -> None:
|
||||
"""
|
||||
Set the default start cell and offsets for the comment. These are
|
||||
generally a fixed offset relative to the parent cell. However there are
|
||||
some edge cases for cells at the, well, edges.
|
||||
"""
|
||||
row_max = 1048576
|
||||
col_max = 16384
|
||||
|
||||
if self.row == 0:
|
||||
self.y_offset = 2
|
||||
self.start_row = 0
|
||||
elif self.row == row_max - 3:
|
||||
self.y_offset = 16
|
||||
self.start_row = row_max - 7
|
||||
elif self.row == row_max - 2:
|
||||
self.y_offset = 16
|
||||
self.start_row = row_max - 6
|
||||
elif self.row == row_max - 1:
|
||||
self.y_offset = 14
|
||||
self.start_row = row_max - 5
|
||||
else:
|
||||
self.y_offset = 10
|
||||
self.start_row = row - 1
|
||||
|
||||
if self.col == col_max - 3:
|
||||
self.x_offset = 49
|
||||
self.start_col = col_max - 6
|
||||
elif self.col == col_max - 2:
|
||||
self.x_offset = 49
|
||||
self.start_col = col_max - 5
|
||||
elif self.col == col_max - 1:
|
||||
self.x_offset = 49
|
||||
self.start_col = col_max - 4
|
||||
else:
|
||||
self.x_offset = 15
|
||||
self.start_col = col + 1
|
||||
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# The file writer class for the Excel XLSX Comments file.
|
||||
#
|
||||
###########################################################################
|
||||
class Comments(xmlwriter.XMLwriter):
|
||||
"""
|
||||
A class for writing the Excel XLSX Comments file.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Public API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self.author_ids = {}
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Private API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _assemble_xml_file(
|
||||
self, comments_data: Optional[List[CommentType]] = None
|
||||
) -> 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) -> None:
|
||||
# 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: List[CommentType]) -> None:
|
||||
# Write the <authors> element.
|
||||
author_count = 0
|
||||
|
||||
self._xml_start_tag("authors")
|
||||
|
||||
for comment in comment_data:
|
||||
author = comment.author
|
||||
|
||||
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: str) -> None:
|
||||
# Write the <author> element.
|
||||
self._xml_data_element("author", data)
|
||||
|
||||
def _write_comment_list(self, comment_data: List[CommentType]) -> None:
|
||||
# Write the <commentList> element.
|
||||
self._xml_start_tag("commentList")
|
||||
|
||||
for comment in comment_data:
|
||||
# Look up the author id.
|
||||
author_id = -1
|
||||
if comment.author is not None:
|
||||
author_id = self.author_ids[comment.author]
|
||||
|
||||
# Write the comment element.
|
||||
self._write_comment(comment, author_id)
|
||||
|
||||
self._xml_end_tag("commentList")
|
||||
|
||||
def _write_comment(self, comment: CommentType, author_id: int) -> None:
|
||||
# Write the <comment> element.
|
||||
ref = xl_rowcol_to_cell(comment.row, comment.col)
|
||||
|
||||
attributes = [("ref", ref)]
|
||||
|
||||
if author_id != -1:
|
||||
attributes.append(("authorId", f"{author_id}"))
|
||||
|
||||
self._xml_start_tag("comment", attributes)
|
||||
|
||||
# Write the text element.
|
||||
self._write_text(comment)
|
||||
|
||||
self._xml_end_tag("comment")
|
||||
|
||||
def _write_text(self, comment: CommentType) -> None:
|
||||
# Write the <text> element.
|
||||
self._xml_start_tag("text")
|
||||
|
||||
# Write the text r element.
|
||||
self._write_text_r(comment)
|
||||
|
||||
self._xml_end_tag("text")
|
||||
|
||||
def _write_text_r(self, comment: CommentType) -> None:
|
||||
# Write the <r> element.
|
||||
self._xml_start_tag("r")
|
||||
|
||||
# Write the rPr element.
|
||||
self._write_r_pr(comment)
|
||||
|
||||
# Write the text r element.
|
||||
self._write_text_t(comment.text)
|
||||
|
||||
self._xml_end_tag("r")
|
||||
|
||||
def _write_text_t(self, text: str) -> None:
|
||||
# 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, comment: CommentType) -> None:
|
||||
# Write the <rPr> element.
|
||||
self._xml_start_tag("rPr")
|
||||
|
||||
# Write the sz element.
|
||||
self._write_sz(comment.font_size)
|
||||
|
||||
# Write the color element.
|
||||
self._write_color()
|
||||
|
||||
# Write the rFont element.
|
||||
self._write_r_font(comment.font_name)
|
||||
|
||||
# Write the family element.
|
||||
self._write_family(comment.font_family)
|
||||
|
||||
self._xml_end_tag("rPr")
|
||||
|
||||
def _write_sz(self, font_size: float) -> None:
|
||||
# Write the <sz> element.
|
||||
attributes = [("val", font_size)]
|
||||
|
||||
self._xml_empty_tag("sz", attributes)
|
||||
|
||||
def _write_color(self) -> None:
|
||||
# Write the <color> element.
|
||||
attributes = [("indexed", 81)]
|
||||
|
||||
self._xml_empty_tag("color", attributes)
|
||||
|
||||
def _write_r_font(self, font_name: str) -> None:
|
||||
# Write the <rFont> element.
|
||||
attributes = [("val", font_name)]
|
||||
|
||||
self._xml_empty_tag("rFont", attributes)
|
||||
|
||||
def _write_family(self, font_family: int) -> None:
|
||||
# Write the <family> element.
|
||||
attributes = [("val", font_family)]
|
||||
|
||||
self._xml_empty_tag("family", attributes)
|
||||
270
venv/lib/python3.12/site-packages/xlsxwriter/contenttypes.py
Normal file
270
venv/lib/python3.12/site-packages/xlsxwriter/contenttypes.py
Normal file
@@ -0,0 +1,270 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 typing import Dict, Tuple
|
||||
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
# 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: Tuple[str, str]) -> None:
|
||||
# Add elements to the ContentTypes defaults.
|
||||
self.defaults.append(default)
|
||||
|
||||
def _add_override(self, override: Tuple[str, str]) -> None:
|
||||
# Add elements to the ContentTypes overrides.
|
||||
self.overrides.append(override)
|
||||
|
||||
def _add_worksheet_name(self, worksheet_name: str) -> None:
|
||||
# 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: str) -> None:
|
||||
# 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: str) -> None:
|
||||
# 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: str) -> None:
|
||||
# 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) -> None:
|
||||
# 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: str) -> None:
|
||||
# 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) -> None:
|
||||
# Add the sharedStrings link to the ContentTypes overrides.
|
||||
self._add_override(
|
||||
("/xl/sharedStrings.xml", APP_DOCUMENT + "spreadsheetml.sharedStrings+xml")
|
||||
)
|
||||
|
||||
def _add_calc_chain(self) -> None:
|
||||
# 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: Dict[str, bool]) -> None:
|
||||
# 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: str) -> None:
|
||||
# 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) -> None:
|
||||
# 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] = ("/xl/workbook.xml", xlsm)
|
||||
|
||||
self._add_default(("bin", "application/vnd.ms-office.vbaProject"))
|
||||
|
||||
def _add_vba_project_signature(self) -> None:
|
||||
# Add a vbaProjectSignature to the ContentTypes overrides.
|
||||
self._add_override(
|
||||
(
|
||||
"/xl/vbaProjectSignature.bin",
|
||||
"application/vnd.ms-office.vbaProjectSignature",
|
||||
)
|
||||
)
|
||||
|
||||
def _add_custom_properties(self) -> None:
|
||||
# Add the custom properties to the ContentTypes overrides.
|
||||
self._add_override(
|
||||
("/docProps/custom.xml", APP_DOCUMENT + "custom-properties+xml")
|
||||
)
|
||||
|
||||
def _add_metadata(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <Default> element.
|
||||
attributes = [
|
||||
("Extension", extension),
|
||||
("ContentType", content_type),
|
||||
]
|
||||
|
||||
self._xml_empty_tag("Default", attributes)
|
||||
|
||||
def _write_override(self, part_name, content_type) -> None:
|
||||
# Write the <Override> element.
|
||||
attributes = [
|
||||
("PartName", part_name),
|
||||
("ContentType", content_type),
|
||||
]
|
||||
|
||||
self._xml_empty_tag("Override", attributes)
|
||||
182
venv/lib/python3.12/site-packages/xlsxwriter/core.py
Normal file
182
venv/lib/python3.12/site-packages/xlsxwriter/core.py
Normal file
@@ -0,0 +1,182 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Union
|
||||
|
||||
from . import xmlwriter
|
||||
|
||||
|
||||
class Core(xmlwriter.XMLwriter):
|
||||
"""
|
||||
A class for writing the Excel XLSX Core file.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Public API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.properties = {}
|
||||
self.iso_date = ""
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Private API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _assemble_xml_file(self) -> None:
|
||||
# Assemble and write the XML file.
|
||||
|
||||
# Set the creation date for the file.
|
||||
date = self.properties.get("created")
|
||||
if not isinstance(date, datetime):
|
||||
date = datetime.now(timezone.utc)
|
||||
|
||||
self.iso_date = date.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# 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: Dict[str, Union[str, datetime]]) -> None:
|
||||
# Set the document properties.
|
||||
self.properties = properties
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# XML methods.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _write_cp_core_properties(self) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <dc:creator> element.
|
||||
data = self.properties.get("author", "")
|
||||
|
||||
self._xml_data_element("dc:creator", data)
|
||||
|
||||
def _write_cp_last_modified_by(self) -> None:
|
||||
# Write the <cp:lastModifiedBy> element.
|
||||
data = self.properties.get("author", "")
|
||||
|
||||
self._xml_data_element("cp:lastModifiedBy", data)
|
||||
|
||||
def _write_dcterms_created(self) -> None:
|
||||
# Write the <dcterms:created> element.
|
||||
attributes = [("xsi:type", "dcterms:W3CDTF")]
|
||||
self._xml_data_element("dcterms:created", self.iso_date, attributes)
|
||||
|
||||
def _write_dcterms_modified(self) -> None:
|
||||
# Write the <dcterms:modified> element.
|
||||
attributes = [("xsi:type", "dcterms:W3CDTF")]
|
||||
self._xml_data_element("dcterms:modified", self.iso_date, attributes)
|
||||
|
||||
def _write_dc_title(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <cp:contentStatus> element.
|
||||
if "status" in self.properties:
|
||||
data = self.properties["status"]
|
||||
else:
|
||||
return
|
||||
|
||||
self._xml_data_element("cp:contentStatus", data)
|
||||
138
venv/lib/python3.12/site-packages/xlsxwriter/custom.py
Normal file
138
venv/lib/python3.12/site-packages/xlsxwriter/custom.py
Normal file
@@ -0,0 +1,138 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 typing import List, Tuple
|
||||
|
||||
from . import xmlwriter
|
||||
|
||||
|
||||
class Custom(xmlwriter.XMLwriter):
|
||||
"""
|
||||
A class for writing the Excel XLSX Custom Workbook Property file.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Public API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.properties = []
|
||||
self.pid = 1
|
||||
|
||||
def _set_properties(self, properties: List[Tuple[str, str, str]]) -> None:
|
||||
# Set the document properties.
|
||||
self.properties = properties
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Private API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _assemble_xml_file(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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: Tuple[str, str, str]) -> None:
|
||||
# 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: str) -> None:
|
||||
# Write the <vt:lpwstr> element.
|
||||
self._xml_data_element("vt:lpwstr", value)
|
||||
|
||||
def _write_vt_filetime(self, value: str) -> None:
|
||||
# Write the <vt:filetime> element.
|
||||
self._xml_data_element("vt:filetime", value)
|
||||
|
||||
def _write_vt_i4(self, value: str) -> None:
|
||||
# Write the <vt:i4> element.
|
||||
self._xml_data_element("vt:i4", value)
|
||||
|
||||
def _write_vt_r8(self, value: str) -> None:
|
||||
# Write the <vt:r8> element.
|
||||
self._xml_data_element("vt:r8", value)
|
||||
|
||||
def _write_vt_bool(self, value: str) -> None:
|
||||
# Write the <vt:bool> element.
|
||||
self._xml_data_element("vt:bool", value)
|
||||
1158
venv/lib/python3.12/site-packages/xlsxwriter/drawing.py
Normal file
1158
venv/lib/python3.12/site-packages/xlsxwriter/drawing.py
Normal file
File diff suppressed because it is too large
Load Diff
56
venv/lib/python3.12/site-packages/xlsxwriter/exceptions.py
Normal file
56
venv/lib/python3.12/site-packages/xlsxwriter/exceptions.py
Normal file
@@ -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()."""
|
||||
@@ -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) -> None:
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.feature_property_bags = set()
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Private API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _assemble_xml_file(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the Checkbox <bag> element.
|
||||
attributes = [("type", "Checkbox")]
|
||||
|
||||
self._xml_empty_tag("bag", attributes)
|
||||
|
||||
def _write_xf_control_bag(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <bagId> element.
|
||||
attributes = []
|
||||
|
||||
if key:
|
||||
attributes = [("k", key)]
|
||||
|
||||
self._xml_data_element("bagId", bag_id, attributes)
|
||||
1274
venv/lib/python3.12/site-packages/xlsxwriter/format.py
Normal file
1274
venv/lib/python3.12/site-packages/xlsxwriter/format.py
Normal file
File diff suppressed because it is too large
Load Diff
401
venv/lib/python3.12/site-packages/xlsxwriter/image.py
Normal file
401
venv/lib/python3.12/site-packages/xlsxwriter/image.py
Normal file
@@ -0,0 +1,401 @@
|
||||
###############################################################################
|
||||
#
|
||||
# Image - A class for representing image objects in Excel.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
#
|
||||
# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
|
||||
#
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from struct import unpack
|
||||
from typing import Tuple, Union
|
||||
|
||||
from xlsxwriter.url import Url
|
||||
|
||||
from .exceptions import UndefinedImageSize, UnsupportedImageFormat
|
||||
|
||||
DEFAULT_DPI = 96.0
|
||||
|
||||
|
||||
class Image:
|
||||
"""
|
||||
A class to represent an image in an Excel worksheet.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, source: Union[str, Path, BytesIO]) -> None:
|
||||
"""
|
||||
Initialize an Image instance.
|
||||
|
||||
Args:
|
||||
source (Union[str, Path, BytesIO]): The filename, Path or BytesIO
|
||||
object of the image.
|
||||
"""
|
||||
if isinstance(source, (str, Path)):
|
||||
self.filename = source
|
||||
self.image_data = None
|
||||
self.image_name = os.path.basename(source)
|
||||
elif isinstance(source, BytesIO):
|
||||
self.filename = ""
|
||||
self.image_data = source
|
||||
self.image_name = ""
|
||||
else:
|
||||
raise ValueError("Source must be a filename (str) or a BytesIO object.")
|
||||
|
||||
self._row: int = 0
|
||||
self._col: int = 0
|
||||
self._x_offset: int = 0
|
||||
self._y_offset: int = 0
|
||||
self._x_scale: float = 1.0
|
||||
self._y_scale: float = 1.0
|
||||
self._url: Union[Url, None] = None
|
||||
self._anchor: int = 2
|
||||
self._description: Union[str, None] = None
|
||||
self._decorative: bool = False
|
||||
self._header_position: Union[str, None] = None
|
||||
self._ref_id: Union[str, None] = None
|
||||
|
||||
# Derived properties.
|
||||
self._image_extension: str = ""
|
||||
self._width: float = 0.0
|
||||
self._height: float = 0.0
|
||||
self._x_dpi: float = DEFAULT_DPI
|
||||
self._y_dpi: float = DEFAULT_DPI
|
||||
self._digest: Union[str, None] = None
|
||||
|
||||
self._get_image_properties()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the main properties of the Image
|
||||
instance.
|
||||
"""
|
||||
return (
|
||||
f"Image:\n"
|
||||
f" filename = {self.filename!r}\n"
|
||||
f" image_name = {self.image_name!r}\n"
|
||||
f" image_type = {self.image_type!r}\n"
|
||||
f" width = {self._width}\n"
|
||||
f" height = {self._height}\n"
|
||||
f" x_dpi = {self._x_dpi}\n"
|
||||
f" y_dpi = {self._y_dpi}\n"
|
||||
)
|
||||
|
||||
@property
|
||||
def image_type(self) -> str:
|
||||
"""Get the image type (e.g., 'PNG', 'JPEG')."""
|
||||
return self._image_extension.upper()
|
||||
|
||||
@property
|
||||
def width(self) -> float:
|
||||
"""Get the width of the image."""
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def height(self) -> float:
|
||||
"""Get the height of the image."""
|
||||
return self._height
|
||||
|
||||
@property
|
||||
def x_dpi(self) -> float:
|
||||
"""Get the horizontal DPI of the image."""
|
||||
return self._x_dpi
|
||||
|
||||
@property
|
||||
def y_dpi(self) -> float:
|
||||
"""Get the vertical DPI of the image."""
|
||||
return self._y_dpi
|
||||
|
||||
@property
|
||||
def description(self) -> Union[str, None]:
|
||||
"""Get the description/alt-text of the image."""
|
||||
return self._description
|
||||
|
||||
@description.setter
|
||||
def description(self, value: str) -> None:
|
||||
"""Set the description/alt-text of the image."""
|
||||
if value:
|
||||
self._description = value
|
||||
|
||||
@property
|
||||
def decorative(self) -> bool:
|
||||
"""Get whether the image is decorative."""
|
||||
return self._decorative
|
||||
|
||||
@decorative.setter
|
||||
def decorative(self, value: bool) -> None:
|
||||
"""Set whether the image is decorative."""
|
||||
self._decorative = value
|
||||
|
||||
@property
|
||||
def url(self) -> Union[Url, None]:
|
||||
"""Get the image url."""
|
||||
return self._url
|
||||
|
||||
@url.setter
|
||||
def url(self, value: Url) -> None:
|
||||
"""Set the image url."""
|
||||
if value:
|
||||
self._url = value
|
||||
|
||||
def _set_user_options(self, options=None) -> None:
|
||||
"""
|
||||
This handles the additional optional parameters to ``insert_button()``.
|
||||
"""
|
||||
if options is None:
|
||||
return
|
||||
|
||||
if not self._url:
|
||||
self._url = Url.from_options(options)
|
||||
if self._url:
|
||||
self._url._set_object_link()
|
||||
|
||||
self._anchor = options.get("object_position", self._anchor)
|
||||
self._x_scale = options.get("x_scale", self._x_scale)
|
||||
self._y_scale = options.get("y_scale", self._y_scale)
|
||||
self._x_offset = options.get("x_offset", self._x_offset)
|
||||
self._y_offset = options.get("y_offset", self._y_offset)
|
||||
self._decorative = options.get("decorative", self._decorative)
|
||||
self.image_data = options.get("image_data", self.image_data)
|
||||
self._description = options.get("description", self._description)
|
||||
|
||||
# For backward compatibility with older parameter name.
|
||||
self._anchor = options.get("positioning", self._anchor)
|
||||
|
||||
def _get_image_properties(self) -> None:
|
||||
# Extract dimension information from the image file.
|
||||
height = 0.0
|
||||
width = 0.0
|
||||
x_dpi = DEFAULT_DPI
|
||||
y_dpi = DEFAULT_DPI
|
||||
|
||||
if self.image_data:
|
||||
# Read the image data from the user supplied byte stream.
|
||||
data = self.image_data.getvalue()
|
||||
else:
|
||||
# Open the image file and read in the data.
|
||||
with open(self.filename, "rb") as fh:
|
||||
data = fh.read()
|
||||
|
||||
# Get the image digest to check for duplicates.
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
|
||||
# Look for some common image file markers.
|
||||
png_marker = unpack("3s", data[1:4])[0]
|
||||
jpg_marker = unpack(">H", data[:2])[0]
|
||||
bmp_marker = unpack("2s", data[:2])[0]
|
||||
gif_marker = unpack("4s", data[:4])[0]
|
||||
emf_marker = (unpack("4s", data[40:44]))[0]
|
||||
emf_marker1 = unpack("<L", data[:4])[0]
|
||||
|
||||
if png_marker == b"PNG":
|
||||
(image_type, width, height, x_dpi, y_dpi) = self._process_png(data)
|
||||
|
||||
elif jpg_marker == 0xFFD8:
|
||||
(image_type, width, height, x_dpi, y_dpi) = self._process_jpg(data)
|
||||
|
||||
elif bmp_marker == b"BM":
|
||||
(image_type, width, height) = self._process_bmp(data)
|
||||
|
||||
elif emf_marker1 == 0x9AC6CDD7:
|
||||
(image_type, width, height, x_dpi, y_dpi) = self._process_wmf(data)
|
||||
|
||||
elif emf_marker1 == 1 and emf_marker == b" EMF":
|
||||
(image_type, width, height, x_dpi, y_dpi) = self._process_emf(data)
|
||||
|
||||
elif gif_marker == b"GIF8":
|
||||
(image_type, width, height, x_dpi, y_dpi) = self._process_gif(data)
|
||||
|
||||
else:
|
||||
raise UnsupportedImageFormat(
|
||||
f"{self.filename}: Unknown or unsupported image file format."
|
||||
)
|
||||
|
||||
# Check that we found the required data.
|
||||
if not height or not width:
|
||||
raise UndefinedImageSize(
|
||||
f"{self.filename}: no size data found in image file."
|
||||
)
|
||||
|
||||
# Set a default dpi for images with 0 dpi.
|
||||
if x_dpi == 0:
|
||||
x_dpi = DEFAULT_DPI
|
||||
if y_dpi == 0:
|
||||
y_dpi = DEFAULT_DPI
|
||||
|
||||
self._image_extension = image_type
|
||||
self._width = width
|
||||
self._height = height
|
||||
self._x_dpi = x_dpi
|
||||
self._y_dpi = y_dpi
|
||||
self._digest = digest
|
||||
|
||||
def _process_png(
|
||||
self,
|
||||
data: bytes,
|
||||
) -> Tuple[str, float, float, float, float]:
|
||||
# Extract width and height information from a PNG file.
|
||||
offset = 8
|
||||
data_length = len(data)
|
||||
end_marker = False
|
||||
width = 0.0
|
||||
height = 0.0
|
||||
x_dpi = DEFAULT_DPI
|
||||
y_dpi = DEFAULT_DPI
|
||||
|
||||
# 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(self, data: bytes) -> Tuple[str, float, float, float, float]:
|
||||
# Extract width and height information from a JPEG file.
|
||||
offset = 2
|
||||
data_length = len(data)
|
||||
end_marker = False
|
||||
width = 0.0
|
||||
height = 0.0
|
||||
x_dpi = DEFAULT_DPI
|
||||
y_dpi = DEFAULT_DPI
|
||||
|
||||
# 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 = DEFAULT_DPI
|
||||
if y_dpi == 1:
|
||||
y_dpi = DEFAULT_DPI
|
||||
|
||||
if marker == 0xFFDA:
|
||||
end_marker = True
|
||||
continue
|
||||
|
||||
offset = offset + length + 2
|
||||
|
||||
return "jpeg", width, height, x_dpi, y_dpi
|
||||
|
||||
def _process_gif(self, data: bytes) -> Tuple[str, float, float, float, float]:
|
||||
# Extract width and height information from a GIF file.
|
||||
x_dpi = DEFAULT_DPI
|
||||
y_dpi = DEFAULT_DPI
|
||||
|
||||
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(self, data: bytes) -> Tuple[str, float, float]:
|
||||
# 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(self, data: bytes) -> Tuple[str, float, float, float, float]:
|
||||
# Extract width and height information from a WMF file.
|
||||
x_dpi = DEFAULT_DPI
|
||||
y_dpi = DEFAULT_DPI
|
||||
|
||||
# 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(self, data: bytes) -> Tuple[str, float, float, float, float]:
|
||||
# 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
|
||||
266
venv/lib/python3.12/site-packages/xlsxwriter/metadata.py
Normal file
266
venv/lib/python3.12/site-packages/xlsxwriter/metadata.py
Normal file
@@ -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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <xda:dynamicArrayProperties> element.
|
||||
attributes = [
|
||||
("fDynamic", 1),
|
||||
("fCollapsed", 0),
|
||||
]
|
||||
|
||||
self._xml_empty_tag("xda:dynamicArrayProperties", attributes)
|
||||
|
||||
def _write_value_ext(self, index) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <xlrd:rvb> element.
|
||||
attributes = [("i", index)]
|
||||
|
||||
self._xml_empty_tag("xlrd:rvb", attributes)
|
||||
|
||||
def _write_cell_metadata(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <rc> element.
|
||||
attributes = [
|
||||
("t", rc_type),
|
||||
("v", index),
|
||||
]
|
||||
|
||||
self._xml_empty_tag("rc", attributes)
|
||||
878
venv/lib/python3.12/site-packages/xlsxwriter/packager.py
Normal file
878
venv/lib/python3.12/site-packages/xlsxwriter/packager.py
Normal file
@@ -0,0 +1,878 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
# Set an optional user defined temp directory.
|
||||
self.tmpdir = tmpdir
|
||||
|
||||
def _set_in_memory(self, in_memory) -> None:
|
||||
# Set the optional 'in_memory' mode.
|
||||
self.in_memory = in_memory
|
||||
|
||||
def _add_workbook(self, workbook) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
|
||||
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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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 in self.workbook.embedded_images.images:
|
||||
image_extension = image.image_type.lower()
|
||||
image_file = f"../media/image{index}.{image_extension}"
|
||||
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) -> None:
|
||||
# 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:
|
||||
xml_image_name = (
|
||||
"xl/media/image" + str(index) + "." + image._image_extension
|
||||
)
|
||||
|
||||
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.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.image_data.getvalue())
|
||||
os_file.close()
|
||||
else:
|
||||
copy(image.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.image_data:
|
||||
# The data is already in a byte stream.
|
||||
os_filename = image.image_data
|
||||
else:
|
||||
image_file = open(image.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) -> None:
|
||||
# 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) -> None:
|
||||
# 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))
|
||||
143
venv/lib/python3.12/site-packages/xlsxwriter/relationships.py
Normal file
143
venv/lib/python3.12/site-packages/xlsxwriter/relationships.py
Normal file
@@ -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) -> None:
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.relationships = []
|
||||
self.id = 1
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Private API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _assemble_xml_file(self) -> None:
|
||||
# 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) -> 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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)
|
||||
98
venv/lib/python3.12/site-packages/xlsxwriter/rich_value.py
Normal file
98
venv/lib/python3.12/site-packages/xlsxwriter/rich_value.py
Normal file
@@ -0,0 +1,98 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
||||
from xlsxwriter.image import Image
|
||||
|
||||
from . import xmlwriter
|
||||
|
||||
|
||||
class RichValue(xmlwriter.XMLwriter):
|
||||
"""
|
||||
A class for writing the Excel XLSX rdrichvalue.xml file.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Public API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self.embedded_images = []
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Private API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _assemble_xml_file(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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 in enumerate(self.embedded_images):
|
||||
# Write the rv element.
|
||||
self._write_rv(index, image)
|
||||
|
||||
def _write_rv(self, index, image: Image) -> None:
|
||||
# Write the <rv> element.
|
||||
attributes = [("s", 0)]
|
||||
value = 5
|
||||
|
||||
if image.decorative:
|
||||
value = 6
|
||||
|
||||
self._xml_start_tag("rv", attributes)
|
||||
|
||||
# Write the v elements.
|
||||
self._write_v(index)
|
||||
self._write_v(value)
|
||||
|
||||
if image.description:
|
||||
self._write_v(image.description)
|
||||
|
||||
self._xml_end_tag("rv")
|
||||
|
||||
def _write_v(self, data) -> None:
|
||||
# Write the <v> element.
|
||||
self._xml_data_element("v", data)
|
||||
@@ -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) -> None:
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self.num_embedded_images = 0
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Private API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _assemble_xml_file(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <rel> element.
|
||||
r_id = f"rId{index}"
|
||||
attributes = [("r:id", r_id)]
|
||||
|
||||
self._xml_empty_tag("rel", attributes)
|
||||
@@ -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) -> None:
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self.has_embedded_descriptions = False
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Private API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _assemble_xml_file(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <k> element.
|
||||
attributes = [
|
||||
("n", name),
|
||||
("t", k_type),
|
||||
]
|
||||
|
||||
self._xml_empty_tag("k", attributes)
|
||||
111
venv/lib/python3.12/site-packages/xlsxwriter/rich_value_types.py
Normal file
111
venv/lib/python3.12/site-packages/xlsxwriter/rich_value_types.py
Normal file
@@ -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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <flag> element.
|
||||
attributes = [
|
||||
("name", name),
|
||||
("value", "1"),
|
||||
]
|
||||
|
||||
self._xml_empty_tag("flag", attributes)
|
||||
433
venv/lib/python3.12/site-packages/xlsxwriter/shape.py
Normal file
433
venv/lib/python3.12/site-packages/xlsxwriter/shape.py
Normal file
@@ -0,0 +1,433 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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
|
||||
|
||||
from xlsxwriter.color import Color
|
||||
|
||||
|
||||
class Shape:
|
||||
"""
|
||||
A class for to represent Excel XLSX shape objects.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Public API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def __init__(self, shape_type, name: str, options) -> None:
|
||||
"""
|
||||
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._set_options(options)
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Private API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _set_options(self, options) -> None:
|
||||
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)
|
||||
|
||||
self.text_rotation = options.get("text_rotation", 0)
|
||||
|
||||
self.textlink = options.get("textlink", "")
|
||||
if self.textlink.startswith("="):
|
||||
self.textlink = self.textlink.lstrip("=")
|
||||
|
||||
# Gradient fill overrides solid fill.
|
||||
if self.gradient:
|
||||
self.fill = None
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Static methods for processing chart/shape style properties.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
@staticmethod
|
||||
def _get_line_properties(options: dict) -> dict:
|
||||
# Convert user line properties to the structure required internally.
|
||||
if not options.get("line") and not options.get("border"):
|
||||
return {"defined": False}
|
||||
|
||||
# Copy the user defined properties since they will be modified.
|
||||
# Depending on the context, the Excel UI property may be called 'line'
|
||||
# or 'border'. Internally they are the same so we handle both.
|
||||
if options.get("line"):
|
||||
line = copy.deepcopy(options["line"])
|
||||
else:
|
||||
line = copy.deepcopy(options["border"])
|
||||
|
||||
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 {}
|
||||
|
||||
if line.get("color"):
|
||||
line["color"] = Color._from_value(line["color"])
|
||||
|
||||
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)
|
||||
|
||||
if fill.get("color"):
|
||||
fill["color"] = Color._from_value(fill["color"])
|
||||
|
||||
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"]]
|
||||
|
||||
if pattern.get("fg_color"):
|
||||
pattern["fg_color"] = Color._from_value(pattern["fg_color"])
|
||||
|
||||
if pattern.get("bg_color"):
|
||||
pattern["bg_color"] = Color._from_value(pattern["bg_color"])
|
||||
else:
|
||||
pattern["bg_color"] = 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"
|
||||
|
||||
gradient["colors"] = [Color._from_value(color) for color in gradient["colors"]]
|
||||
|
||||
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)
|
||||
|
||||
if font.get("color"):
|
||||
font["color"] = Color._from_value(font["color"])
|
||||
|
||||
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.get("name") is not None:
|
||||
attributes.append(("typeface", font["name"]))
|
||||
|
||||
if font.get("pitch_family") is not None:
|
||||
attributes.append(("pitchFamily", font["pitch_family"]))
|
||||
|
||||
if font.get("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
|
||||
138
venv/lib/python3.12/site-packages/xlsxwriter/sharedstrings.py
Normal file
138
venv/lib/python3.12/site-packages/xlsxwriter/sharedstrings.py
Normal file
@@ -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) -> None:
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.string_table = None
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Private API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _assemble_xml_file(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the sst string elements.
|
||||
|
||||
for string in self.string_table.string_array:
|
||||
self._write_si(string)
|
||||
|
||||
def _write_si(self, string) -> None:
|
||||
# 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) -> None:
|
||||
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) -> None:
|
||||
""" " 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 = {}
|
||||
788
venv/lib/python3.12/site-packages/xlsxwriter/styles.py
Normal file
788
venv/lib/python3.12/site-packages/xlsxwriter/styles.py
Normal file
@@ -0,0 +1,788 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
# 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) -> None:
|
||||
# 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]
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# XML methods.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _write_style_sheet(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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 = xf_format.font_color
|
||||
if not color._is_automatic:
|
||||
self._write_color(color._attributes())
|
||||
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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <vertAlign> font sub-element.
|
||||
attributes = [("val", val)]
|
||||
|
||||
self._xml_empty_tag("vertAlign", attributes)
|
||||
|
||||
def _write_color(self, attributes) -> None:
|
||||
# Write the <color> element.
|
||||
self._xml_empty_tag("color", attributes)
|
||||
|
||||
def _write_fills(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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:
|
||||
if not fg_color._is_automatic:
|
||||
self._xml_empty_tag("fgColor", fg_color._attributes())
|
||||
|
||||
if bg_color:
|
||||
if not bg_color._is_automatic:
|
||||
self._xml_empty_tag("bgColor", bg_color._attributes())
|
||||
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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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 not color._is_automatic:
|
||||
self._xml_empty_tag("color", color._attributes())
|
||||
else:
|
||||
self._xml_empty_tag("color", [("auto", 1)])
|
||||
|
||||
self._xml_end_tag(border_type)
|
||||
|
||||
def _write_cell_style_xfs(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <cellStyle> element.
|
||||
attributes = [
|
||||
("name", name),
|
||||
("xfId", xf_id),
|
||||
("builtinId", builtin_id),
|
||||
]
|
||||
|
||||
self._xml_empty_tag("cellStyle", attributes)
|
||||
|
||||
def _write_dxfs(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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:
|
||||
# For backwards compatibility convert possible
|
||||
self._write_color(color._attributes())
|
||||
|
||||
self._xml_end_tag("mruColors")
|
||||
|
||||
def _write_condense(self) -> None:
|
||||
# Write the <condense> element.
|
||||
attributes = [("val", 0)]
|
||||
|
||||
self._xml_empty_tag("condense", attributes)
|
||||
|
||||
def _write_extend(self) -> None:
|
||||
# Write the <extend> element.
|
||||
attributes = [("val", 0)]
|
||||
|
||||
self._xml_empty_tag("extend", attributes)
|
||||
|
||||
def _write_xf_format_extensions(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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")
|
||||
194
venv/lib/python3.12/site-packages/xlsxwriter/table.py
Normal file
194
venv/lib/python3.12/site-packages/xlsxwriter/table.py
Normal file
@@ -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) -> None:
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.properties = {}
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Private API.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _assemble_xml_file(self) -> None:
|
||||
# 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) -> None:
|
||||
# Set the document properties.
|
||||
self.properties = properties
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# XML methods.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
def _write_table(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <calculatedColumnFormula> element.
|
||||
self._xml_data_element("calculatedColumnFormula", formula)
|
||||
|
||||
def _write_totals_row_formula(self, formula) -> None:
|
||||
# Write the <totalsRowFormula> element.
|
||||
self._xml_data_element("totalsRowFormula", formula)
|
||||
69
venv/lib/python3.12/site-packages/xlsxwriter/theme.py
Normal file
69
venv/lib/python3.12/site-packages/xlsxwriter/theme.py
Normal file
File diff suppressed because one or more lines are too long
268
venv/lib/python3.12/site-packages/xlsxwriter/url.py
Normal file
268
venv/lib/python3.12/site-packages/xlsxwriter/url.py
Normal file
@@ -0,0 +1,268 @@
|
||||
###############################################################################
|
||||
#
|
||||
# Url - A class to represent URLs in Excel.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
#
|
||||
# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
|
||||
#
|
||||
|
||||
|
||||
import re
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class UrlTypes(Enum):
|
||||
"""
|
||||
Enum to represent different types of URLS.
|
||||
"""
|
||||
|
||||
UNKNOWN = 0
|
||||
URL = 1
|
||||
INTERNAL = 2
|
||||
EXTERNAL = 3
|
||||
|
||||
|
||||
class Url:
|
||||
"""
|
||||
A class to represent URLs in Excel.
|
||||
|
||||
"""
|
||||
|
||||
MAX_URL_LEN = 2080
|
||||
MAX_PARAMETER_LEN = 255
|
||||
|
||||
def __init__(self, link: str) -> None:
|
||||
self._link_type: UrlTypes = UrlTypes.UNKNOWN
|
||||
self._original_url: str = link
|
||||
self._link: str = link
|
||||
self._relationship_link: str = link
|
||||
self._text: str = ""
|
||||
self._tip: str = ""
|
||||
self._anchor: str = ""
|
||||
self._is_object_link: bool = False
|
||||
self._rel_index: int = 0
|
||||
|
||||
self._parse_url()
|
||||
|
||||
if len(self._link) > self.MAX_URL_LEN:
|
||||
raise ValueError("URL exceeds Excel's maximum length.")
|
||||
|
||||
if len(self._anchor) > self.MAX_URL_LEN:
|
||||
raise ValueError("Anchor segment or url exceeds Excel's maximum length.")
|
||||
|
||||
if len(self._tip) > self.MAX_PARAMETER_LEN:
|
||||
raise ValueError("Hyperlink tool tip exceeds Excel's maximum length.")
|
||||
|
||||
self._escape_strings()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return a string representation of the Url instance.
|
||||
|
||||
"""
|
||||
return (
|
||||
"\n"
|
||||
f"Url:\n"
|
||||
f" _link_type = {self._link_type.name}\n"
|
||||
f" _original_url = {self._original_url}\n"
|
||||
f" _link = {self._link}\n"
|
||||
f" _relationship_link = {self._relationship_link}\n"
|
||||
f" _text = {self._text}\n"
|
||||
f" _tip = {self._tip}\n"
|
||||
f" _anchor = {self._anchor}\n"
|
||||
f" _is_object_link = {self._is_object_link}\n"
|
||||
f" _rel_index = {self._rel_index}\n"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_options(cls, options: Dict[str, Any]) -> Optional["Url"]:
|
||||
"""
|
||||
For backward compatibility, convert the 'url' key and 'tip' keys in an
|
||||
options dictionary to a Url object, or return the Url object if already
|
||||
an instance.
|
||||
|
||||
Args:
|
||||
options (dict): A dictionary that may contain a 'url' key.
|
||||
|
||||
Returns:
|
||||
url: A Url object or None.
|
||||
|
||||
"""
|
||||
if not isinstance(options, dict):
|
||||
raise TypeError("The 'options' parameter must be a dictionary.")
|
||||
|
||||
url = options.get("url")
|
||||
|
||||
if isinstance(url, str):
|
||||
url = cls(options["url"])
|
||||
if options.get("tip"):
|
||||
url._tip = options["tip"]
|
||||
|
||||
return url
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
"""Get the alternative, user-friendly, text for the URL."""
|
||||
return self._text
|
||||
|
||||
@text.setter
|
||||
def text(self, value: str) -> None:
|
||||
"""Set the alternative, user-friendly, text for the URL."""
|
||||
self._text = value
|
||||
|
||||
@property
|
||||
def tip(self) -> str:
|
||||
"""Get the screen tip for the URL."""
|
||||
return self._tip
|
||||
|
||||
@tip.setter
|
||||
def tip(self, value: str) -> None:
|
||||
"""Set the screen tip for the URL."""
|
||||
self._tip = value
|
||||
|
||||
def _parse_url(self) -> None:
|
||||
"""Parse the URL and determine its type."""
|
||||
|
||||
# Handle mail address links.
|
||||
if self._link.startswith("mailto:"):
|
||||
self._link_type = UrlTypes.URL
|
||||
|
||||
if not self._text:
|
||||
self._text = self._link.replace("mailto:", "", 1)
|
||||
|
||||
# Handle links to cells within the workbook.
|
||||
elif self._link.startswith("internal:"):
|
||||
self._link_type = UrlTypes.INTERNAL
|
||||
self._relationship_link = self._link.replace("internal:", "#", 1)
|
||||
self._link = self._link.replace("internal:", "", 1)
|
||||
self._anchor = self._link
|
||||
|
||||
if not self._text:
|
||||
self._text = self._anchor
|
||||
|
||||
# Handle links to other files or cells in other Excel files.
|
||||
elif self._link.startswith("file://") or self._link.startswith("external:"):
|
||||
self._link_type = UrlTypes.EXTERNAL
|
||||
|
||||
# Handle backward compatibility with external: links.
|
||||
file_url = self._original_url.replace("external:", "file:///", 1)
|
||||
|
||||
link_path = file_url
|
||||
link_path = link_path.replace("file:///", "", 1)
|
||||
link_path = link_path.replace("file://", "", 1)
|
||||
link_path = link_path.replace("/", "\\")
|
||||
|
||||
if self._is_relative_path(link_path):
|
||||
self._link = link_path
|
||||
else:
|
||||
self._link = "file:///" + link_path
|
||||
|
||||
if not self._text:
|
||||
self._text = link_path
|
||||
|
||||
if "#" in self._link:
|
||||
self._link, self._anchor = self._link.split("#", 1)
|
||||
|
||||
# Set up the relationship link. This doesn't usually contain the
|
||||
# anchor unless it is a link from an object like an image.
|
||||
if self._is_object_link:
|
||||
if self._is_relative_path(link_path):
|
||||
self._relationship_link = self._link.replace("\\", "/")
|
||||
else:
|
||||
self._relationship_link = file_url
|
||||
|
||||
else:
|
||||
self._relationship_link = self._link
|
||||
|
||||
# Convert a .\dir\file.xlsx link to dir\file.xlsx.
|
||||
if self._relationship_link.startswith(".\\"):
|
||||
self._relationship_link = self._relationship_link.replace(".\\", "", 1)
|
||||
|
||||
# Handle standard Excel links like http://, https://, ftp://, ftps://
|
||||
# but also allow custom "foo://bar" URLs.
|
||||
elif "://" in self._link:
|
||||
self._link_type = UrlTypes.URL
|
||||
|
||||
if not self._text:
|
||||
self._text = self._link
|
||||
|
||||
if "#" in self._link:
|
||||
self._link, self._anchor = self._link.split("#", 1)
|
||||
|
||||
# Set up the relationship link. This doesn't usually contain the
|
||||
# anchor unless it is a link from an object like an image.
|
||||
if self._is_object_link:
|
||||
self._relationship_link = self._original_url
|
||||
else:
|
||||
self._relationship_link = self._link
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown URL type: {self._original_url}")
|
||||
|
||||
def _set_object_link(self) -> None:
|
||||
"""
|
||||
Set the _is_object_link flag and re-parse the URL since the relationship
|
||||
link is different for object links.
|
||||
|
||||
"""
|
||||
self._is_object_link = True
|
||||
self._link = self._original_url
|
||||
self._parse_url()
|
||||
self._escape_strings()
|
||||
|
||||
def _escape_strings(self) -> None:
|
||||
"""Escape special characters in the URL strings."""
|
||||
|
||||
if self._link_type != UrlTypes.INTERNAL:
|
||||
self._link = self._escape_url(self._link)
|
||||
self._relationship_link = self._escape_url(self._relationship_link)
|
||||
|
||||
# Excel additionally escapes # to %23 in file paths.
|
||||
if self._link_type == UrlTypes.EXTERNAL:
|
||||
self._relationship_link = self._relationship_link.replace("#", "%23")
|
||||
|
||||
def _target(self) -> str:
|
||||
"""Get the target for relationship IDs."""
|
||||
return self._relationship_link
|
||||
|
||||
def _target_mode(self) -> str:
|
||||
"""Get the target mode for relationship IDs."""
|
||||
if self._link_type == UrlTypes.INTERNAL:
|
||||
return ""
|
||||
|
||||
return "External"
|
||||
|
||||
@staticmethod
|
||||
def _is_relative_path(url: str) -> bool:
|
||||
"""Check if a URL is a relative path."""
|
||||
if url.startswith(r"\\"):
|
||||
return False
|
||||
|
||||
if url[0].isalpha() and url[1] == ":":
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _escape_url(url: str) -> str:
|
||||
"""Escape special characters in a 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.
|
||||
return (
|
||||
url.replace("%", "%25")
|
||||
.replace('"', "%22")
|
||||
.replace(" ", "%20")
|
||||
.replace("<", "%3c")
|
||||
.replace(">", "%3e")
|
||||
.replace("[", "%5b")
|
||||
.replace("]", "%5d")
|
||||
.replace("^", "%5e")
|
||||
.replace("`", "%60")
|
||||
.replace("{", "%7b")
|
||||
.replace("}", "%7d")
|
||||
)
|
||||
954
venv/lib/python3.12/site-packages/xlsxwriter/utility.py
Normal file
954
venv/lib/python3.12/site-packages/xlsxwriter/utility.py
Normal file
@@ -0,0 +1,954 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 re
|
||||
from typing import Dict, Optional, Tuple, Union
|
||||
from warnings import warn
|
||||
|
||||
from xlsxwriter.color import Color
|
||||
|
||||
COL_NAMES: Dict[int, str] = {}
|
||||
|
||||
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: int,
|
||||
col: int,
|
||||
row_abs: bool = False,
|
||||
col_abs: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
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 ""
|
||||
|
||||
if col < 0:
|
||||
warn(f"Col number '{col}' must be >= 0")
|
||||
return ""
|
||||
|
||||
row += 1 # Change to 1-index.
|
||||
row_abs_str = "$" if row_abs else ""
|
||||
|
||||
col_str = xl_col_to_name(col, col_abs)
|
||||
|
||||
return col_str + row_abs_str + str(row)
|
||||
|
||||
|
||||
def xl_rowcol_to_cell_fast(row: int, col: int) -> str:
|
||||
"""
|
||||
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: int, col_abs: bool = False) -> str:
|
||||
"""
|
||||
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 ""
|
||||
|
||||
col_num += 1 # Change to 1-index.
|
||||
col_str = ""
|
||||
col_abs_str = "$" 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_str + col_str
|
||||
|
||||
|
||||
def xl_cell_to_rowcol(cell_str: str) -> Tuple[int, int]:
|
||||
"""
|
||||
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)
|
||||
if match is None:
|
||||
warn(f"Invalid cell reference '{cell_str}'")
|
||||
return 0, 0
|
||||
|
||||
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: str) -> Tuple[int, int, bool, bool]:
|
||||
"""
|
||||
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)
|
||||
if match is None:
|
||||
warn(f"Invalid cell reference '{cell_str}'")
|
||||
return 0, 0, False, False
|
||||
|
||||
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: int, first_col: int, last_row: int, last_col: int) -> str:
|
||||
"""
|
||||
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 == "" or range2 == "":
|
||||
warn("Row and column numbers must be >= 0")
|
||||
return ""
|
||||
|
||||
if range1 == range2:
|
||||
return range1
|
||||
|
||||
return range1 + ":" + range2
|
||||
|
||||
|
||||
def xl_range_abs(first_row: int, first_col: int, last_row: int, last_col: int) -> str:
|
||||
"""
|
||||
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 == "" or range2 == "":
|
||||
warn("Row and column numbers must be >= 0")
|
||||
return ""
|
||||
|
||||
if range1 == range2:
|
||||
return range1
|
||||
|
||||
return range1 + ":" + range2
|
||||
|
||||
|
||||
def xl_range_formula(
|
||||
sheetname: str, first_row: int, first_col: int, last_row: int, last_col: int
|
||||
) -> str:
|
||||
"""
|
||||
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: str) -> str:
|
||||
"""
|
||||
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("'"):
|
||||
|
||||
match_rule3 = RE_QUOTE_RULE3.match(uppercase_sheetname)
|
||||
match_rule4_row = RE_QUOTE_RULE4_ROW.match(uppercase_sheetname)
|
||||
match_rule4_column = RE_QUOTE_RULE4_COLUMN.match(uppercase_sheetname)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 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 match_rule3:
|
||||
cell = match_rule3.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 match_rule4_row:
|
||||
row = int(match_rule4_row.group(1))
|
||||
|
||||
if 0 < row <= row_max:
|
||||
requires_quoting = True
|
||||
|
||||
# Rule 4b. Check for sheet names that start with C1 or RC1 style
|
||||
elif match_rule4_column:
|
||||
col = int(match_rule4_column.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: str) -> int:
|
||||
"""
|
||||
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: str) -> int:
|
||||
"""
|
||||
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 _get_sparkline_style(style_id: int) -> Dict[str, Dict[str, str]]:
|
||||
"""
|
||||
Get the numbered sparkline styles.
|
||||
|
||||
"""
|
||||
styles = [
|
||||
{ # 0
|
||||
"low": Color.theme(4, 0),
|
||||
"high": Color.theme(4, 0),
|
||||
"last": Color.theme(4, 3),
|
||||
"first": Color.theme(4, 3),
|
||||
"series": Color.theme(4, 5),
|
||||
"markers": Color.theme(4, 5),
|
||||
"negative": Color.theme(5, 0),
|
||||
},
|
||||
{ # 1
|
||||
"low": Color.theme(4, 0),
|
||||
"high": Color.theme(4, 0),
|
||||
"last": Color.theme(4, 3),
|
||||
"first": Color.theme(4, 3),
|
||||
"series": Color.theme(4, 5),
|
||||
"markers": Color.theme(4, 5),
|
||||
"negative": Color.theme(5, 0),
|
||||
},
|
||||
{ # 2
|
||||
"low": Color.theme(5, 0),
|
||||
"high": Color.theme(5, 0),
|
||||
"last": Color.theme(5, 3),
|
||||
"first": Color.theme(5, 3),
|
||||
"series": Color.theme(5, 5),
|
||||
"markers": Color.theme(5, 5),
|
||||
"negative": Color.theme(6, 0),
|
||||
},
|
||||
{ # 3
|
||||
"low": Color.theme(6, 0),
|
||||
"high": Color.theme(6, 0),
|
||||
"last": Color.theme(6, 3),
|
||||
"first": Color.theme(6, 3),
|
||||
"series": Color.theme(6, 5),
|
||||
"markers": Color.theme(6, 5),
|
||||
"negative": Color.theme(7, 0),
|
||||
},
|
||||
{ # 4
|
||||
"low": Color.theme(7, 0),
|
||||
"high": Color.theme(7, 0),
|
||||
"last": Color.theme(7, 3),
|
||||
"first": Color.theme(7, 3),
|
||||
"series": Color.theme(7, 5),
|
||||
"markers": Color.theme(7, 5),
|
||||
"negative": Color.theme(8, 0),
|
||||
},
|
||||
{ # 5
|
||||
"low": Color.theme(8, 0),
|
||||
"high": Color.theme(8, 0),
|
||||
"last": Color.theme(8, 3),
|
||||
"first": Color.theme(8, 3),
|
||||
"series": Color.theme(8, 5),
|
||||
"markers": Color.theme(8, 5),
|
||||
"negative": Color.theme(9, 0),
|
||||
},
|
||||
{ # 6
|
||||
"low": Color.theme(9, 0),
|
||||
"high": Color.theme(9, 0),
|
||||
"last": Color.theme(9, 3),
|
||||
"first": Color.theme(9, 3),
|
||||
"series": Color.theme(9, 5),
|
||||
"markers": Color.theme(9, 5),
|
||||
"negative": Color.theme(4, 0),
|
||||
},
|
||||
{ # 7
|
||||
"low": Color.theme(5, 4),
|
||||
"high": Color.theme(5, 4),
|
||||
"last": Color.theme(5, 4),
|
||||
"first": Color.theme(5, 4),
|
||||
"series": Color.theme(4, 4),
|
||||
"markers": Color.theme(5, 4),
|
||||
"negative": Color.theme(5, 0),
|
||||
},
|
||||
{ # 8
|
||||
"low": Color.theme(6, 4),
|
||||
"high": Color.theme(6, 4),
|
||||
"last": Color.theme(6, 4),
|
||||
"first": Color.theme(6, 4),
|
||||
"series": Color.theme(5, 4),
|
||||
"markers": Color.theme(6, 4),
|
||||
"negative": Color.theme(6, 0),
|
||||
},
|
||||
{ # 9
|
||||
"low": Color.theme(7, 4),
|
||||
"high": Color.theme(7, 4),
|
||||
"last": Color.theme(7, 4),
|
||||
"first": Color.theme(7, 4),
|
||||
"series": Color.theme(6, 4),
|
||||
"markers": Color.theme(7, 4),
|
||||
"negative": Color.theme(7, 0),
|
||||
},
|
||||
{ # 10
|
||||
"low": Color.theme(8, 4),
|
||||
"high": Color.theme(8, 4),
|
||||
"last": Color.theme(8, 4),
|
||||
"first": Color.theme(8, 4),
|
||||
"series": Color.theme(7, 4),
|
||||
"markers": Color.theme(8, 4),
|
||||
"negative": Color.theme(8, 0),
|
||||
},
|
||||
{ # 11
|
||||
"low": Color.theme(9, 4),
|
||||
"high": Color.theme(9, 4),
|
||||
"last": Color.theme(9, 4),
|
||||
"first": Color.theme(9, 4),
|
||||
"series": Color.theme(8, 4),
|
||||
"markers": Color.theme(9, 4),
|
||||
"negative": Color.theme(9, 0),
|
||||
},
|
||||
{ # 12
|
||||
"low": Color.theme(4, 4),
|
||||
"high": Color.theme(4, 4),
|
||||
"last": Color.theme(4, 4),
|
||||
"first": Color.theme(4, 4),
|
||||
"series": Color.theme(9, 4),
|
||||
"markers": Color.theme(4, 4),
|
||||
"negative": Color.theme(4, 0),
|
||||
},
|
||||
{ # 13
|
||||
"low": Color.theme(4, 4),
|
||||
"high": Color.theme(4, 4),
|
||||
"last": Color.theme(4, 4),
|
||||
"first": Color.theme(4, 4),
|
||||
"series": Color.theme(4, 0),
|
||||
"markers": Color.theme(4, 4),
|
||||
"negative": Color.theme(5, 0),
|
||||
},
|
||||
{ # 14
|
||||
"low": Color.theme(5, 4),
|
||||
"high": Color.theme(5, 4),
|
||||
"last": Color.theme(5, 4),
|
||||
"first": Color.theme(5, 4),
|
||||
"series": Color.theme(5, 0),
|
||||
"markers": Color.theme(5, 4),
|
||||
"negative": Color.theme(6, 0),
|
||||
},
|
||||
{ # 15
|
||||
"low": Color.theme(6, 4),
|
||||
"high": Color.theme(6, 4),
|
||||
"last": Color.theme(6, 4),
|
||||
"first": Color.theme(6, 4),
|
||||
"series": Color.theme(6, 0),
|
||||
"markers": Color.theme(6, 4),
|
||||
"negative": Color.theme(7, 0),
|
||||
},
|
||||
{ # 16
|
||||
"low": Color.theme(7, 4),
|
||||
"high": Color.theme(7, 4),
|
||||
"last": Color.theme(7, 4),
|
||||
"first": Color.theme(7, 4),
|
||||
"series": Color.theme(7, 0),
|
||||
"markers": Color.theme(7, 4),
|
||||
"negative": Color.theme(8, 0),
|
||||
},
|
||||
{ # 17
|
||||
"low": Color.theme(8, 4),
|
||||
"high": Color.theme(8, 4),
|
||||
"last": Color.theme(8, 4),
|
||||
"first": Color.theme(8, 4),
|
||||
"series": Color.theme(8, 0),
|
||||
"markers": Color.theme(8, 4),
|
||||
"negative": Color.theme(9, 0),
|
||||
},
|
||||
{ # 18
|
||||
"low": Color.theme(9, 4),
|
||||
"high": Color.theme(9, 4),
|
||||
"last": Color.theme(9, 4),
|
||||
"first": Color.theme(9, 4),
|
||||
"series": Color.theme(9, 0),
|
||||
"markers": Color.theme(9, 4),
|
||||
"negative": Color.theme(4, 0),
|
||||
},
|
||||
{ # 19
|
||||
"low": Color.theme(4, 5),
|
||||
"high": Color.theme(4, 5),
|
||||
"last": Color.theme(4, 4),
|
||||
"first": Color.theme(4, 4),
|
||||
"series": Color.theme(4, 3),
|
||||
"markers": Color.theme(4, 1),
|
||||
"negative": Color.theme(0, 5),
|
||||
},
|
||||
{ # 20
|
||||
"low": Color.theme(5, 5),
|
||||
"high": Color.theme(5, 5),
|
||||
"last": Color.theme(5, 4),
|
||||
"first": Color.theme(5, 4),
|
||||
"series": Color.theme(5, 3),
|
||||
"markers": Color.theme(5, 1),
|
||||
"negative": Color.theme(0, 5),
|
||||
},
|
||||
{ # 21
|
||||
"low": Color.theme(6, 5),
|
||||
"high": Color.theme(6, 5),
|
||||
"last": Color.theme(6, 4),
|
||||
"first": Color.theme(6, 4),
|
||||
"series": Color.theme(6, 3),
|
||||
"markers": Color.theme(6, 1),
|
||||
"negative": Color.theme(0, 5),
|
||||
},
|
||||
{ # 22
|
||||
"low": Color.theme(7, 5),
|
||||
"high": Color.theme(7, 5),
|
||||
"last": Color.theme(7, 4),
|
||||
"first": Color.theme(7, 4),
|
||||
"series": Color.theme(7, 3),
|
||||
"markers": Color.theme(7, 1),
|
||||
"negative": Color.theme(0, 5),
|
||||
},
|
||||
{ # 23
|
||||
"low": Color.theme(8, 5),
|
||||
"high": Color.theme(8, 5),
|
||||
"last": Color.theme(8, 4),
|
||||
"first": Color.theme(8, 4),
|
||||
"series": Color.theme(8, 3),
|
||||
"markers": Color.theme(8, 1),
|
||||
"negative": Color.theme(0, 5),
|
||||
},
|
||||
{ # 24
|
||||
"low": Color.theme(9, 5),
|
||||
"high": Color.theme(9, 5),
|
||||
"last": Color.theme(9, 4),
|
||||
"first": Color.theme(9, 4),
|
||||
"series": Color.theme(9, 3),
|
||||
"markers": Color.theme(9, 1),
|
||||
"negative": Color.theme(0, 5),
|
||||
},
|
||||
{ # 25
|
||||
"low": Color.theme(1, 3),
|
||||
"high": Color.theme(1, 3),
|
||||
"last": Color.theme(1, 3),
|
||||
"first": Color.theme(1, 3),
|
||||
"series": Color.theme(1, 1),
|
||||
"markers": Color.theme(1, 3),
|
||||
"negative": Color.theme(1, 3),
|
||||
},
|
||||
{ # 26
|
||||
"low": Color.theme(0, 3),
|
||||
"high": Color.theme(0, 3),
|
||||
"last": Color.theme(0, 3),
|
||||
"first": Color.theme(0, 3),
|
||||
"series": Color.theme(1, 2),
|
||||
"markers": Color.theme(0, 3),
|
||||
"negative": Color.theme(0, 3),
|
||||
},
|
||||
{ # 27
|
||||
"low": Color("#D00000"),
|
||||
"high": Color("#D00000"),
|
||||
"last": Color("#D00000"),
|
||||
"first": Color("#D00000"),
|
||||
"series": Color("#323232"),
|
||||
"markers": Color("#D00000"),
|
||||
"negative": Color("#D00000"),
|
||||
},
|
||||
{ # 28
|
||||
"low": Color("#0070C0"),
|
||||
"high": Color("#0070C0"),
|
||||
"last": Color("#0070C0"),
|
||||
"first": Color("#0070C0"),
|
||||
"series": Color("#000000"),
|
||||
"markers": Color("#0070C0"),
|
||||
"negative": Color("#0070C0"),
|
||||
},
|
||||
{ # 29
|
||||
"low": Color("#D00000"),
|
||||
"high": Color("#D00000"),
|
||||
"last": Color("#D00000"),
|
||||
"first": Color("#D00000"),
|
||||
"series": Color("#376092"),
|
||||
"markers": Color("#D00000"),
|
||||
"negative": Color("#D00000"),
|
||||
},
|
||||
{ # 30
|
||||
"low": Color("#000000"),
|
||||
"high": Color("#000000"),
|
||||
"last": Color("#000000"),
|
||||
"first": Color("#000000"),
|
||||
"series": Color("#0070C0"),
|
||||
"markers": Color("#000000"),
|
||||
"negative": Color("#000000"),
|
||||
},
|
||||
{ # 31
|
||||
"low": Color("#FF5055"),
|
||||
"high": Color("#56BE79"),
|
||||
"last": Color("#359CEB"),
|
||||
"first": Color("#5687C2"),
|
||||
"series": Color("#5F5F5F"),
|
||||
"markers": Color("#D70077"),
|
||||
"negative": Color("#FFB620"),
|
||||
},
|
||||
{ # 32
|
||||
"low": Color("#FF5055"),
|
||||
"high": Color("#56BE79"),
|
||||
"last": Color("#359CEB"),
|
||||
"first": Color("#777777"),
|
||||
"series": Color("#5687C2"),
|
||||
"markers": Color("#D70077"),
|
||||
"negative": Color("#FFB620"),
|
||||
},
|
||||
{ # 33
|
||||
"low": Color("#FF5367"),
|
||||
"high": Color("#60D276"),
|
||||
"last": Color("#FFEB9C"),
|
||||
"first": Color("#FFDC47"),
|
||||
"series": Color("#C6EFCE"),
|
||||
"markers": Color("#8CADD6"),
|
||||
"negative": Color("#FFC7CE"),
|
||||
},
|
||||
{ # 34
|
||||
"low": Color("#FF0000"),
|
||||
"high": Color("#00B050"),
|
||||
"last": Color("#FFC000"),
|
||||
"first": Color("#FFC000"),
|
||||
"series": Color("#00B050"),
|
||||
"markers": Color("#0070C0"),
|
||||
"negative": Color("#FF0000"),
|
||||
},
|
||||
{ # 35
|
||||
"low": Color.theme(7, 0),
|
||||
"high": Color.theme(6, 0),
|
||||
"last": Color.theme(5, 0),
|
||||
"first": Color.theme(4, 0),
|
||||
"series": Color.theme(3, 0),
|
||||
"markers": Color.theme(8, 0),
|
||||
"negative": Color.theme(9, 0),
|
||||
},
|
||||
{ # 36
|
||||
"low": Color.theme(7, 0),
|
||||
"high": Color.theme(6, 0),
|
||||
"last": Color.theme(5, 0),
|
||||
"first": Color.theme(4, 0),
|
||||
"series": Color.theme(1, 0),
|
||||
"markers": Color.theme(8, 0),
|
||||
"negative": Color.theme(9, 0),
|
||||
},
|
||||
]
|
||||
|
||||
return styles[style_id]
|
||||
|
||||
|
||||
def _supported_datetime(
|
||||
dt: Union[datetime.datetime, datetime.time, datetime.date],
|
||||
) -> bool:
|
||||
# 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: datetime.datetime, remove_timezone: bool
|
||||
) -> datetime.datetime:
|
||||
# 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: Union[datetime.time, datetime.datetime, datetime.timedelta, datetime.date],
|
||||
date_1904: bool,
|
||||
remove_timezone: bool,
|
||||
) -> float:
|
||||
# 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 not isinstance(dt_obj, datetime.timedelta)
|
||||
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: str) -> Optional[re.Match]:
|
||||
# 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)
|
||||
783
venv/lib/python3.12/site-packages/xlsxwriter/vml.py
Normal file
783
venv/lib/python3.12/site-packages/xlsxwriter/vml.py
Normal file
@@ -0,0 +1,783 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 xlsxwriter.comments import CommentType
|
||||
from xlsxwriter.image import Image
|
||||
|
||||
from . import xmlwriter
|
||||
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# A button type class.
|
||||
#
|
||||
###########################################################################
|
||||
class ButtonType:
|
||||
"""
|
||||
A class to represent a button in an Excel worksheet.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
row: int,
|
||||
col: int,
|
||||
height: int,
|
||||
width: int,
|
||||
button_number: int,
|
||||
options: dict = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize a ButtonType instance.
|
||||
|
||||
Args:
|
||||
row (int): The row number of the button.
|
||||
col (int): The column number of the button.
|
||||
height (int): The height of the button.
|
||||
width (int): The width of the button.
|
||||
button_number (int): The button number.
|
||||
options (dict): Additional options for the button.
|
||||
"""
|
||||
self.row = row
|
||||
self.col = col
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
self.macro = f"[0]!Button{button_number}_Click"
|
||||
self.caption = f"Button {button_number}"
|
||||
self.description = None
|
||||
|
||||
self.x_scale = 1
|
||||
self.y_scale = 1
|
||||
self.x_offset = 0
|
||||
self.y_offset = 0
|
||||
|
||||
self.vertices = []
|
||||
|
||||
# Set any user supplied options.
|
||||
self._set_user_options(options)
|
||||
|
||||
def _set_user_options(self, options=None) -> None:
|
||||
"""
|
||||
This method handles the additional optional parameters to
|
||||
``insert_button()``.
|
||||
"""
|
||||
if options is None:
|
||||
return
|
||||
|
||||
# Overwrite the defaults with any user supplied values. Incorrect or
|
||||
# misspelled parameters are silently ignored.
|
||||
self.width = options.get("width", self.width)
|
||||
self.height = options.get("height", self.height)
|
||||
self.caption = options.get("caption", self.caption)
|
||||
self.x_offset = options.get("x_offset", self.x_offset)
|
||||
self.y_offset = options.get("y_offset", self.y_offset)
|
||||
self.description = options.get("description", self.description)
|
||||
|
||||
# Set the macro name.
|
||||
if options.get("macro"):
|
||||
self.macro = "[0]!" + options["macro"]
|
||||
|
||||
# Scale the size of the button box if required.
|
||||
if options.get("x_scale"):
|
||||
self.width = self.width * options["x_scale"]
|
||||
|
||||
if options.get("y_scale"):
|
||||
self.height = self.height * options["y_scale"]
|
||||
|
||||
# Round the dimensions to the nearest pixel.
|
||||
self.width = int(0.5 + self.width)
|
||||
self.height = int(0.5 + self.height)
|
||||
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# The file writer class for the Excel XLSX VML file.
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
|
||||
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=None,
|
||||
) -> 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:
|
||||
# Write the v:shapetype element.
|
||||
self._write_image_shapetype()
|
||||
|
||||
index = 1
|
||||
for image in header_images:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <o:idmap> element.
|
||||
attributes = [
|
||||
("v:ext", "edit"),
|
||||
("data", data_id),
|
||||
]
|
||||
|
||||
self._xml_empty_tag("o:idmap", attributes)
|
||||
|
||||
def _write_comment_shapetype(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <v:stroke> element.
|
||||
joinstyle = "miter"
|
||||
|
||||
attributes = [("joinstyle", joinstyle)]
|
||||
|
||||
self._xml_empty_tag("v:stroke", attributes)
|
||||
|
||||
def _write_comment_path(self, gradientshapeok, connecttype) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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: CommentType) -> None:
|
||||
# Write the <v:shape> element.
|
||||
shape_type = "#_x0000_t202"
|
||||
insetmode = "auto"
|
||||
visibility = "hidden"
|
||||
|
||||
# Set the shape index.
|
||||
shape_id = "_x0000_s" + str(shape_id)
|
||||
|
||||
(left, top, width, height) = self._pixels_to_points(comment.vertices)
|
||||
|
||||
# Set the visibility.
|
||||
if comment.is_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", comment.color._vml_rgb_hex_value()),
|
||||
("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(comment)
|
||||
|
||||
self._xml_end_tag("v:shape")
|
||||
|
||||
def _write_button_shape(self, shape_id, z_index, button: ButtonType) -> None:
|
||||
# Write the <v:shape> element.
|
||||
shape_type = "#_x0000_t201"
|
||||
|
||||
# Set the shape index.
|
||||
shape_id = "_x0000_s" + str(shape_id)
|
||||
|
||||
(left, top, width, height) = self._pixels_to_points(button.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.description is not None:
|
||||
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)
|
||||
|
||||
# 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: Image) -> None:
|
||||
# Write the <v:shape> element.
|
||||
shape_type = "#_x0000_t75"
|
||||
|
||||
# Set the shape index.
|
||||
shape_id = "_x0000_s" + str(shape_id)
|
||||
|
||||
# Get the image parameters
|
||||
name = image.image_name
|
||||
width = image._width
|
||||
x_dpi = image._x_dpi
|
||||
y_dpi = image._y_dpi
|
||||
height = image._height
|
||||
ref_id = image._ref_id
|
||||
position = image._header_position
|
||||
|
||||
# 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) -> None:
|
||||
# Write the <v:fill> element.
|
||||
color_2 = "#ffffe1"
|
||||
|
||||
attributes = [("color2", color_2)]
|
||||
|
||||
self._xml_empty_tag("v:fill", attributes)
|
||||
|
||||
def _write_button_fill(self) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# 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, button: ButtonType) -> None:
|
||||
# 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", button.caption)
|
||||
|
||||
self._xml_end_tag("v:textbox")
|
||||
|
||||
def _write_div(self, align: str, caption: str = None) -> None:
|
||||
# Write the <div> element.
|
||||
|
||||
style = "text-align:" + align
|
||||
|
||||
attributes = [("style", style)]
|
||||
|
||||
self._xml_start_tag("div", attributes)
|
||||
|
||||
if caption:
|
||||
self._write_button_font(caption)
|
||||
|
||||
self._xml_end_tag("div")
|
||||
|
||||
def _write_button_font(self, caption: str) -> None:
|
||||
# Write the <font> element.
|
||||
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, comment: CommentType) -> None:
|
||||
# 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(comment.vertices)
|
||||
|
||||
# Write the x:AutoFill element.
|
||||
self._write_auto_fill()
|
||||
|
||||
# Write the x:Row element.
|
||||
self._write_row(comment.row)
|
||||
|
||||
# Write the x:Column element.
|
||||
self._write_column(comment.col)
|
||||
|
||||
# Write the x:Visible element.
|
||||
if comment.is_visible:
|
||||
self._write_visible()
|
||||
|
||||
self._xml_end_tag("x:ClientData")
|
||||
|
||||
def _write_button_client_data(self, button) -> None:
|
||||
# Write the <x:ClientData> element.
|
||||
object_type = "Button"
|
||||
|
||||
attributes = [("ObjectType", object_type)]
|
||||
|
||||
self._xml_start_tag("x:ClientData", attributes)
|
||||
|
||||
# Write the x:Anchor element.
|
||||
self._write_anchor(button.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(button.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) -> None:
|
||||
# Write the <x:MoveWithCells> element.
|
||||
self._xml_empty_tag("x:MoveWithCells")
|
||||
|
||||
def _write_size_with_cells(self) -> None:
|
||||
# Write the <x:SizeWithCells> element.
|
||||
self._xml_empty_tag("x:SizeWithCells")
|
||||
|
||||
def _write_visible(self) -> None:
|
||||
# Write the <x:Visible> element.
|
||||
self._xml_empty_tag("x:Visible")
|
||||
|
||||
def _write_anchor(self, vertices) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <x:AutoFill> element.
|
||||
data = "False"
|
||||
|
||||
self._xml_data_element("x:AutoFill", data)
|
||||
|
||||
def _write_row(self, data) -> None:
|
||||
# Write the <x:Row> element.
|
||||
self._xml_data_element("x:Row", data)
|
||||
|
||||
def _write_column(self, data) -> None:
|
||||
# Write the <x:Column> element.
|
||||
self._xml_data_element("x:Column", data)
|
||||
|
||||
def _write_print_object(self) -> None:
|
||||
# Write the <x:PrintObject> element.
|
||||
self._xml_data_element("x:PrintObject", "False")
|
||||
|
||||
def _write_text_halign(self) -> None:
|
||||
# Write the <x:TextHAlign> element.
|
||||
self._xml_data_element("x:TextHAlign", "Center")
|
||||
|
||||
def _write_text_valign(self) -> None:
|
||||
# Write the <x:TextVAlign> element.
|
||||
self._xml_data_element("x:TextVAlign", "Center")
|
||||
|
||||
def _write_fmla_macro(self, data) -> None:
|
||||
# Write the <x:FmlaMacro> element.
|
||||
self._xml_data_element("x:FmlaMacro", data)
|
||||
|
||||
def _write_imagedata(self, ref_id, o_title) -> None:
|
||||
# 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) -> None:
|
||||
# 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) -> None:
|
||||
# Write the <v:f> element.
|
||||
attributes = [("eqn", eqn)]
|
||||
|
||||
self._xml_empty_tag("v:f", attributes)
|
||||
1817
venv/lib/python3.12/site-packages/xlsxwriter/workbook.py
Normal file
1817
venv/lib/python3.12/site-packages/xlsxwriter/workbook.py
Normal file
File diff suppressed because it is too large
Load Diff
8381
venv/lib/python3.12/site-packages/xlsxwriter/worksheet.py
Normal file
8381
venv/lib/python3.12/site-packages/xlsxwriter/worksheet.py
Normal file
File diff suppressed because it is too large
Load Diff
235
venv/lib/python3.12/site-packages/xlsxwriter/xmlwriter.py
Normal file
235
venv/lib/python3.12/site-packages/xlsxwriter/xmlwriter.py
Normal file
@@ -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) -> None:
|
||||
self.fh = None
|
||||
self.internal_fh = False
|
||||
|
||||
def _set_filehandle(self, filehandle) -> None:
|
||||
# Set the writer filehandle directly. Mainly for testing.
|
||||
self.fh = filehandle
|
||||
self.internal_fh = False
|
||||
|
||||
def _set_xml_writer(self, filename) -> None:
|
||||
# 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) -> None:
|
||||
# Close the XML filehandle if we created it.
|
||||
if self.internal_fh:
|
||||
self.fh.close()
|
||||
|
||||
def _xml_declaration(self) -> None:
|
||||
# Write the XML declaration.
|
||||
self.fh.write('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n')
|
||||
|
||||
def _xml_start_tag(self, tag, attributes=[]) -> None:
|
||||
# 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=[]) -> None:
|
||||
# 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) -> None:
|
||||
# Write an XML end tag.
|
||||
self.fh.write(f"</{tag}>")
|
||||
|
||||
def _xml_empty_tag(self, tag, attributes=[]) -> None:
|
||||
# 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=[]) -> None:
|
||||
# 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=[]) -> None:
|
||||
# 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=[]) -> None:
|
||||
# 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=[]) -> None:
|
||||
# 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) -> None:
|
||||
# Optimized tag writer for shared strings <si> rich string elements.
|
||||
|
||||
self.fh.write(f"<si>{string}</si>")
|
||||
|
||||
def _xml_number_element(self, number, attributes=[]) -> None:
|
||||
# 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=[]) -> None:
|
||||
# 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=[]) -> None:
|
||||
# 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=[]) -> None:
|
||||
# Optimized tag writer for rich inlineStr in the inner loop.
|
||||
attr = ""
|
||||
|
||||
for key, value in attributes:
|
||||
value = self._escape_attributes(value)
|
||||
attr += f' {key}="{value}"'
|
||||
|
||||
self.fh.write(f'<c{attr} t="inlineStr"><is>{string}</is></c>')
|
||||
|
||||
def _escape_attributes(self, attribute):
|
||||
# Escape XML characters in attributes.
|
||||
try:
|
||||
if not xml_escapes.search(attribute):
|
||||
return attribute
|
||||
except TypeError:
|
||||
return attribute
|
||||
|
||||
attribute = (
|
||||
attribute.replace("&", "&")
|
||||
.replace('"', """)
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\n", "
")
|
||||
)
|
||||
return attribute
|
||||
|
||||
def _escape_data(self, data):
|
||||
# Escape XML characters in data sections of tags. Note, this
|
||||
# is different from _escape_attributes() in that double quotes
|
||||
# are not escaped by Excel.
|
||||
try:
|
||||
if not xml_escapes.search(data):
|
||||
return data
|
||||
except TypeError:
|
||||
return data
|
||||
|
||||
data = data.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _escape_control_characters(data):
|
||||
# Excel escapes control characters with _xHHHH_ and also escapes any
|
||||
# literal strings of that type by encoding the leading underscore.
|
||||
# So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_.
|
||||
# The following substitutions deal with those cases.
|
||||
try:
|
||||
# Escape the escape.
|
||||
data = re_control_chars_1.sub(r"_x005F\1", data)
|
||||
except TypeError:
|
||||
return data
|
||||
|
||||
# Convert control character to the _xHHHH_ escape.
|
||||
data = re_control_chars_2.sub(
|
||||
lambda match: f"_x{ord(match.group(1)):04X}_", data
|
||||
)
|
||||
|
||||
# Escapes non characters in strings.
|
||||
data = data.replace("\ufffe", "_xFFFE_").replace("\uffff", "_xFFFF_")
|
||||
|
||||
return data
|
||||
Reference in New Issue
Block a user