init: 폴더구조 설계 및 인프라 설계
This commit is contained in:
@@ -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"]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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]]
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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 = {}
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
File diff suppressed because one or more lines are too long
@@ -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")
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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