initial commit

This commit is contained in:
Nico Melone
2023-08-24 17:49:47 -05:00
parent 23b7279c0f
commit 72d3f2c348
5422 changed files with 890638 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
__version__ = "3.1.2"
__VERSION__ = __version__
from .workbook import Workbook # noqa

View File

@@ -0,0 +1,198 @@
###############################################################################
#
# App - A class for writing the Excel XLSX App file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
# Package imports.
from . import xmlwriter
class App(xmlwriter.XMLwriter):
"""
A class for writing the Excel XLSX App file.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self):
"""
Constructor.
"""
super(App, self).__init__()
self.part_names = []
self.heading_pairs = []
self.properties = {}
self.doc_security = 0
def _add_part_name(self, part_name):
# Add the name of a workbook Part such as 'Sheet1' or 'Print_Titles'.
self.part_names.append(part_name)
def _add_heading_pair(self, heading_pair):
# Add the name of a workbook Heading Pair such as 'Worksheets',
# 'Charts' or 'Named Ranges'.
# Ignore empty pairs such as chartsheets.
if not heading_pair[1]:
return
self.heading_pairs.append(("lpstr", heading_pair[0]))
self.heading_pairs.append(("i4", heading_pair[1]))
def _set_properties(self, properties):
# Set the document properties.
self.properties = properties
###########################################################################
#
# Private API.
#
###########################################################################
def _assemble_xml_file(self):
# Assemble and write the XML file.
# Write the XML declaration.
self._xml_declaration()
self._write_properties()
self._write_application()
self._write_doc_security()
self._write_scale_crop()
self._write_heading_pairs()
self._write_titles_of_parts()
self._write_manager()
self._write_company()
self._write_links_up_to_date()
self._write_shared_doc()
self._write_hyperlink_base()
self._write_hyperlinks_changed()
self._write_app_version()
self._xml_end_tag("Properties")
# Close the file.
self._xml_close()
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_properties(self):
# Write the <Properties> element.
schema = "http://schemas.openxmlformats.org/officeDocument/2006/"
xmlns = schema + "extended-properties"
xmlns_vt = schema + "docPropsVTypes"
attributes = [
("xmlns", xmlns),
("xmlns:vt", xmlns_vt),
]
self._xml_start_tag("Properties", attributes)
def _write_application(self):
# Write the <Application> element.
self._xml_data_element("Application", "Microsoft Excel")
def _write_doc_security(self):
# Write the <DocSecurity> element.
self._xml_data_element("DocSecurity", self.doc_security)
def _write_scale_crop(self):
# Write the <ScaleCrop> element.
self._xml_data_element("ScaleCrop", "false")
def _write_heading_pairs(self):
# Write the <HeadingPairs> element.
self._xml_start_tag("HeadingPairs")
self._write_vt_vector("variant", self.heading_pairs)
self._xml_end_tag("HeadingPairs")
def _write_titles_of_parts(self):
# Write the <TitlesOfParts> element.
parts_data = []
self._xml_start_tag("TitlesOfParts")
for part_name in self.part_names:
parts_data.append(("lpstr", part_name))
self._write_vt_vector("lpstr", parts_data)
self._xml_end_tag("TitlesOfParts")
def _write_vt_vector(self, base_type, vector_data):
# Write the <vt:vector> element.
attributes = [
("size", len(vector_data)),
("baseType", base_type),
]
self._xml_start_tag("vt:vector", attributes)
for vt_data in vector_data:
if base_type == "variant":
self._xml_start_tag("vt:variant")
self._write_vt_data(vt_data)
if base_type == "variant":
self._xml_end_tag("vt:variant")
self._xml_end_tag("vt:vector")
def _write_vt_data(self, vt_data):
# Write the <vt:*> elements such as <vt:lpstr> and <vt:if>.
self._xml_data_element("vt:%s" % vt_data[0], vt_data[1])
def _write_company(self):
company = self.properties.get("company", "")
self._xml_data_element("Company", company)
def _write_manager(self):
# Write the <Manager> element.
if "manager" not in self.properties:
return
self._xml_data_element("Manager", self.properties["manager"])
def _write_links_up_to_date(self):
# Write the <LinksUpToDate> element.
self._xml_data_element("LinksUpToDate", "false")
def _write_shared_doc(self):
# Write the <SharedDoc> element.
self._xml_data_element("SharedDoc", "false")
def _write_hyperlink_base(self):
# Write the <HyperlinkBase> element.
hyperlink_base = self.properties.get("hyperlink_base")
if hyperlink_base is None:
return
self._xml_data_element("HyperlinkBase", hyperlink_base)
def _write_hyperlinks_changed(self):
# Write the <HyperlinksChanged> element.
self._xml_data_element("HyperlinksChanged", "false")
def _write_app_version(self):
# Write the <AppVersion> element.
self._xml_data_element("AppVersion", "12.0000")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,101 @@
###############################################################################
#
# ChartArea - A class for writing the Excel XLSX Area charts.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
from . import chart
class ChartArea(chart.Chart):
"""
A class for writing the Excel XLSX Area charts.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self, options=None):
"""
Constructor.
"""
super(ChartArea, self).__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 = 0
# Override and reset the default axis values.
if self.subtype == "percent_stacked":
self.y_axis["defaults"]["num_format"] = "0%"
# Set the available data label positions for this chart type.
self.label_position_default = "center"
self.label_positions = {"center": "ctr"}
self.set_y_axis({})
###########################################################################
#
# Private API.
#
###########################################################################
def _write_chart_type(self, args):
# Override the virtual superclass method with a chart specific method.
# Write the c:areaChart element.
self._write_area_chart(args)
###########################################################################
#
# XML methods.
#
###########################################################################
#
def _write_area_chart(self, args):
# Write the <c:areaChart> element.
if args["primary_axes"]:
series = self._get_primary_axes_series()
else:
series = self._get_secondary_axes_series()
if not len(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")

View File

@@ -0,0 +1,173 @@
###############################################################################
#
# ChartBar - A class for writing the Excel XLSX Bar charts.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
from . import chart
from warnings import warn
class ChartBar(chart.Chart):
"""
A class for writing the Excel XLSX Bar charts.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self, options=None):
"""
Constructor.
"""
super(ChartBar, self).__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 = 0
# Override and reset the default axis values.
self.x_axis["defaults"]["major_gridlines"] = {"visible": 1}
self.y_axis["defaults"]["major_gridlines"] = {"visible": 0}
if self.subtype == "percent_stacked":
self.x_axis["defaults"]["num_format"] = "0%"
# Set the available data label positions for this chart type.
self.label_position_default = "outside_end"
self.label_positions = {
"center": "ctr",
"inside_base": "inBase",
"inside_end": "inEnd",
"outside_end": "outEnd",
}
self.set_x_axis({})
self.set_y_axis({})
def combine(self, chart=None):
"""
Create a combination chart with a secondary chart.
Note: Override parent method to add an extra check that is required
for Bar charts to ensure that their combined chart is on a secondary
axis.
Args:
chart: The secondary chart to combine with the primary chart.
Returns:
Nothing.
"""
if chart is None:
return
if not chart.is_secondary:
warn("Charts combined with Bar charts must be on a secondary axis")
self.combined = chart
###########################################################################
#
# Private API.
#
###########################################################################
def _write_chart_type(self, args):
# Override the virtual superclass method with a chart specific method.
if args["primary_axes"]:
# Reverse X and Y axes for Bar charts.
tmp = self.y_axis
self.y_axis = self.x_axis
self.x_axis = tmp
if self.y2_axis["position"] == "r":
self.y2_axis["position"] = "t"
# Write the c:barChart element.
self._write_bar_chart(args)
def _write_bar_chart(self, args):
# Write the <c:barChart> element.
if args["primary_axes"]:
series = self._get_primary_axes_series()
else:
series = self._get_secondary_axes_series()
if not len(series):
return
subtype = self.subtype
if subtype == "percent_stacked":
subtype = "percentStacked"
# Set a default overlap for stacked charts.
if "stacked" in self.subtype and self.series_overlap_1 is None:
self.series_overlap_1 = 100
self._xml_start_tag("c:barChart")
# Write the c:barDir element.
self._write_bar_dir()
# Write the c:grouping element.
self._write_grouping(subtype)
# Write the c:ser elements.
for data in series:
self._write_ser(data)
# Write the c:gapWidth element.
if args["primary_axes"]:
self._write_gap_width(self.series_gap_1)
else:
self._write_gap_width(self.series_gap_2)
# Write the c:overlap element.
if args["primary_axes"]:
self._write_overlap(self.series_overlap_1)
else:
self._write_overlap(self.series_overlap_2)
# Write the c:axId elements
self._write_axis_ids(args)
self._xml_end_tag("c:barChart")
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_bar_dir(self):
# Write the <c:barDir> element.
val = "bar"
attributes = [("val", val)]
self._xml_empty_tag("c:barDir", attributes)
def _write_err_dir(self, val):
# Overridden from Chart class since it is not used in Bar charts.
pass

View File

@@ -0,0 +1,132 @@
###############################################################################
#
# ChartColumn - A class for writing the Excel XLSX Column charts.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
from . import chart
class ChartColumn(chart.Chart):
"""
A class for writing the Excel XLSX Column charts.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self, options=None):
"""
Constructor.
"""
super(ChartColumn, self).__init__()
if options is None:
options = {}
self.subtype = options.get("subtype")
if not self.subtype:
self.subtype = "clustered"
self.horiz_val_axis = 0
if self.subtype == "percent_stacked":
self.y_axis["defaults"]["num_format"] = "0%"
# Set the available data label positions for this chart type.
self.label_position_default = "outside_end"
self.label_positions = {
"center": "ctr",
"inside_base": "inBase",
"inside_end": "inEnd",
"outside_end": "outEnd",
}
self.set_y_axis({})
###########################################################################
#
# Private API.
#
###########################################################################
def _write_chart_type(self, args):
# Override the virtual superclass method with a chart specific method.
# Write the c:barChart element.
self._write_bar_chart(args)
def _write_bar_chart(self, args):
# Write the <c:barChart> element.
if args["primary_axes"]:
series = self._get_primary_axes_series()
else:
series = self._get_secondary_axes_series()
if not len(series):
return
subtype = self.subtype
if subtype == "percent_stacked":
subtype = "percentStacked"
# Set a default overlap for stacked charts.
if "stacked" in self.subtype and self.series_overlap_1 is None:
self.series_overlap_1 = 100
self._xml_start_tag("c:barChart")
# Write the c:barDir element.
self._write_bar_dir()
# Write the c:grouping element.
self._write_grouping(subtype)
# Write the c:ser elements.
for data in series:
self._write_ser(data)
# Write the c:gapWidth element.
if args["primary_axes"]:
self._write_gap_width(self.series_gap_1)
else:
self._write_gap_width(self.series_gap_2)
# Write the c:overlap element.
if args["primary_axes"]:
self._write_overlap(self.series_overlap_1)
else:
self._write_overlap(self.series_overlap_2)
# Write the c:axId elements
self._write_axis_ids(args)
self._xml_end_tag("c:barChart")
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_bar_dir(self):
# Write the <c:barDir> element.
val = "col"
attributes = [("val", val)]
self._xml_empty_tag("c:barDir", attributes)
def _write_err_dir(self, val):
# Overridden from Chart class since it is not used in Column charts.
pass

View File

@@ -0,0 +1,99 @@
###############################################################################
#
# ChartDoughnut - A class for writing the Excel XLSX Doughnut charts.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, 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, options=None):
"""
Constructor.
"""
super(ChartDoughnut, self).__init__()
self.vary_data_color = 1
self.rotation = 0
self.hole_size = 50
def set_hole_size(self, size):
"""
Set the Doughnut chart hole size.
Args:
size: 10 <= size <= 90.
Returns:
Nothing.
"""
if size is None:
return
# Ensure the size is in Excel's range.
if size < 10 or size > 90:
warn("Chart hole size %d outside Excel range: 10 <= size <= 90" % size)
return
self.hole_size = int(size)
###########################################################################
#
# Private API.
#
###########################################################################
def _write_chart_type(self, args):
# Override the virtual superclass method with a chart specific method.
# Write the c:doughnutChart element.
self._write_doughnut_chart(args)
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_doughnut_chart(self, args):
# Write the <c:doughnutChart> element. Over-ridden method to remove
# axis_id code since Doughnut charts don't require val and cat axes.
self._xml_start_tag("c:doughnutChart")
# Write the c:varyColors element.
self._write_vary_colors()
# Write the series elements.
for data in self.series:
self._write_ser(data)
# Write the c:firstSliceAng element.
self._write_first_slice_ang()
# Write the c:holeSize element.
self._write_c_hole_size()
self._xml_end_tag("c:doughnutChart")
def _write_c_hole_size(self):
# Write the <c:holeSize> element.
attributes = [("val", self.hole_size)]
self._xml_empty_tag("c:holeSize", attributes)

View File

@@ -0,0 +1,143 @@
###############################################################################
#
# ChartLine - A class for writing the Excel XLSX Line charts.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
from . import chart
class ChartLine(chart.Chart):
"""
A class for writing the Excel XLSX Line charts.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self, options=None):
"""
Constructor.
"""
super(ChartLine, self).__init__()
if options is None:
options = {}
self.subtype = options.get("subtype")
if not self.subtype:
self.subtype = "standard"
self.default_marker = {"type": "none"}
self.smooth_allowed = True
# Override and reset the default axis values.
if self.subtype == "percent_stacked":
self.y_axis["defaults"]["num_format"] = "0%"
# Set the available data label positions for this chart type.
self.label_position_default = "right"
self.label_positions = {
"center": "ctr",
"right": "r",
"left": "l",
"above": "t",
"below": "b",
# For backward compatibility.
"top": "t",
"bottom": "b",
}
self.set_y_axis({})
###########################################################################
#
# Private API.
#
###########################################################################
def _write_chart_type(self, args):
# Override the virtual superclass method with a chart specific method.
# Write the c:lineChart element.
self._write_line_chart(args)
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_line_chart(self, args):
# Write the <c:lineChart> element.
if args["primary_axes"]:
series = self._get_primary_axes_series()
else:
series = self._get_secondary_axes_series()
if not len(series):
return
subtype = self.subtype
if subtype == "percent_stacked":
subtype = "percentStacked"
self._xml_start_tag("c:lineChart")
# Write the c:grouping element.
self._write_grouping(subtype)
# Write the series elements.
for data in series:
self._write_ser(data)
# Write the c:dropLines element.
self._write_drop_lines()
# Write the c:hiLowLines element.
self._write_hi_low_lines()
# Write the c:upDownBars element.
self._write_up_down_bars()
# Write the c:marker element.
self._write_marker_value()
# Write the c:axId elements
self._write_axis_ids(args)
self._xml_end_tag("c:lineChart")
def _write_d_pt_point(self, index, point):
# Write an individual <c:dPt> element. Override the parent method to
# add markers.
self._xml_start_tag("c:dPt")
# Write the c:idx element.
self._write_idx(index)
self._xml_start_tag("c:marker")
# Write the c:spPr element.
self._write_sp_pr(point)
self._xml_end_tag("c:marker")
self._xml_end_tag("c:dPt")
def _write_marker_value(self):
# Write the <c:marker> element without a sub-element.
attributes = [("val", 1)]
self._xml_empty_tag("c:marker", attributes)

View File

@@ -0,0 +1,251 @@
###############################################################################
#
# ChartPie - A class for writing the Excel XLSX Pie charts.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, 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, options=None):
"""
Constructor.
"""
super(ChartPie, self).__init__()
self.vary_data_color = 1
self.rotation = 0
# Set the available data label positions for this chart type.
self.label_position_default = "best_fit"
self.label_positions = {
"center": "ctr",
"inside_end": "inEnd",
"outside_end": "outEnd",
"best_fit": "bestFit",
}
def set_rotation(self, rotation):
"""
Set the Pie/Doughnut chart rotation: the angle of the first slice.
Args:
rotation: First segment angle: 0 <= rotation <= 360.
Returns:
Nothing.
"""
if rotation is None:
return
# Ensure the rotation is in Excel's range.
if rotation < 0 or rotation > 360:
warn(
"Chart rotation %d outside Excel range: 0 <= rotation <= 360" % rotation
)
return
self.rotation = int(rotation)
###########################################################################
#
# Private API.
#
###########################################################################
def _write_chart_type(self, args):
# Override the virtual superclass method with a chart specific method.
# Write the c:pieChart element.
self._write_pie_chart(args)
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_pie_chart(self, args):
# Write the <c:pieChart> element. Over-ridden method to remove
# axis_id code since Pie charts don't require val and cat axes.
self._xml_start_tag("c:pieChart")
# Write the c:varyColors element.
self._write_vary_colors()
# Write the series elements.
for data in self.series:
self._write_ser(data)
# Write the c:firstSliceAng element.
self._write_first_slice_ang()
self._xml_end_tag("c:pieChart")
def _write_plot_area(self):
# Over-ridden method to remove the cat_axis() and val_axis() code
# since Pie charts don't require those axes.
#
# Write the <c:plotArea> element.
self._xml_start_tag("c:plotArea")
# Write the c:layout element.
self._write_layout(self.plotarea.get("layout"), "plot")
# Write the subclass chart type element.
self._write_chart_type(None)
# Configure a combined chart if present.
second_chart = self.combined
if second_chart:
# Secondary axis has unique id otherwise use same as primary.
if second_chart.is_secondary:
second_chart.id = 1000 + self.id
else:
second_chart.id = self.id
# Share the same filehandle for writing.
second_chart.fh = self.fh
# Share series index with primary chart.
second_chart.series_index = self.series_index
# Write the subclass chart type elements for combined chart.
second_chart._write_chart_type(None)
# Write the c:spPr element for the plotarea formatting.
self._write_sp_pr(self.plotarea)
self._xml_end_tag("c:plotArea")
def _write_legend(self):
# Over-ridden method to add <c:txPr> to legend.
# Write the <c:legend> element.
legend = self.legend
position = legend.get("position", "right")
font = legend.get("font")
delete_series = []
overlay = 0
if legend.get("delete_series") and type(legend["delete_series"]) is list:
delete_series = legend["delete_series"]
if position.startswith("overlay_"):
position = position.replace("overlay_", "")
overlay = 1
allowed = {
"right": "r",
"left": "l",
"top": "t",
"bottom": "b",
"top_right": "tr",
}
if position == "none":
return
if position not in allowed:
return
position = allowed[position]
self._xml_start_tag("c:legend")
# Write the c:legendPos element.
self._write_legend_pos(position)
# Remove series labels from the legend.
for index in delete_series:
# Write the c:legendEntry element.
self._write_legend_entry(index)
# Write the c:layout element.
self._write_layout(legend.get("layout"), "legend")
# Write the c:overlay element.
if overlay:
self._write_overlay()
# Write the c:spPr element.
self._write_sp_pr(legend)
# Write the c:txPr element. Over-ridden.
self._write_tx_pr_legend(None, font)
self._xml_end_tag("c:legend")
def _write_tx_pr_legend(self, horiz, font):
# Write the <c:txPr> element for legends.
if font and font.get("rotation"):
rotation = font["rotation"]
else:
rotation = None
self._xml_start_tag("c:txPr")
# Write the a:bodyPr element.
self._write_a_body_pr(rotation, horiz)
# Write the a:lstStyle element.
self._write_a_lst_style()
# Write the a:p element.
self._write_a_p_legend(font)
self._xml_end_tag("c:txPr")
def _write_a_p_legend(self, font):
# Write the <a:p> element for legends.
self._xml_start_tag("a:p")
# Write the a:pPr element.
self._write_a_p_pr_legend(font)
# Write the a:endParaRPr element.
self._write_a_end_para_rpr()
self._xml_end_tag("a:p")
def _write_a_p_pr_legend(self, font):
# Write the <a:pPr> element for legends.
attributes = [("rtl", 0)]
self._xml_start_tag("a:pPr", attributes)
# Write the a:defRPr element.
self._write_a_def_rpr(font)
self._xml_end_tag("a:pPr")
def _write_vary_colors(self):
# Write the <c:varyColors> element.
attributes = [("val", 1)]
self._xml_empty_tag("c:varyColors", attributes)
def _write_first_slice_ang(self):
# Write the <c:firstSliceAng> element.
attributes = [("val", self.rotation)]
self._xml_empty_tag("c:firstSliceAng", attributes)

View File

@@ -0,0 +1,102 @@
###############################################################################
#
# ChartRadar - A class for writing the Excel XLSX Radar charts.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
from . import chart
class ChartRadar(chart.Chart):
"""
A class for writing the Excel XLSX Radar charts.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self, options=None):
"""
Constructor.
"""
super(ChartRadar, self).__init__()
if options is None:
options = {}
self.subtype = options.get("subtype")
if not self.subtype:
self.subtype = "marker"
self.default_marker = {"type": "none"}
# Override and reset the default axis values.
self.x_axis["defaults"]["major_gridlines"] = {"visible": 1}
self.set_x_axis({})
# Set the available data label positions for this chart type.
self.label_position_default = "center"
self.label_positions = {"center": "ctr"}
# Hardcode major_tick_mark for now until there is an accessor.
self.y_axis["major_tick_mark"] = "cross"
###########################################################################
#
# Private API.
#
###########################################################################
def _write_chart_type(self, args):
# Write the c:radarChart element.
self._write_radar_chart(args)
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_radar_chart(self, args):
# Write the <c:radarChart> element.
if args["primary_axes"]:
series = self._get_primary_axes_series()
else:
series = self._get_secondary_axes_series()
if not len(series):
return
self._xml_start_tag("c:radarChart")
# Write the c:radarStyle element.
self._write_radar_style()
# Write the series elements.
for data in series:
self._write_ser(data)
# Write the c:axId elements
self._write_axis_ids(args)
self._xml_end_tag("c:radarChart")
def _write_radar_style(self):
# Write the <c:radarStyle> element.
val = "marker"
if self.subtype == "filled":
val = "filled"
attributes = [("val", val)]
self._xml_empty_tag("c:radarStyle", attributes)

View File

@@ -0,0 +1,333 @@
###############################################################################
#
# ChartScatter - A class for writing the Excel XLSX Scatter charts.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
from . import chart
from warnings import warn
class ChartScatter(chart.Chart):
"""
A class for writing the Excel XLSX Scatter charts.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self, options=None):
"""
Constructor.
"""
super(ChartScatter, self).__init__()
if options is None:
options = {}
self.subtype = options.get("subtype")
if not self.subtype:
self.subtype = "marker_only"
self.cross_between = "midCat"
self.horiz_val_axis = 0
self.val_axis_position = "b"
self.smooth_allowed = True
self.requires_category = True
# Set the available data label positions for this chart type.
self.label_position_default = "right"
self.label_positions = {
"center": "ctr",
"right": "r",
"left": "l",
"above": "t",
"below": "b",
# For backward compatibility.
"top": "t",
"bottom": "b",
}
def combine(self, chart=None):
"""
Create a combination chart with a secondary chart.
Note: Override parent method to add a warning.
Args:
chart: The secondary chart to combine with the primary chart.
Returns:
Nothing.
"""
if chart is None:
return
warn(
"Combined chart not currently supported with scatter chart "
"as the primary chart"
)
###########################################################################
#
# Private API.
#
###########################################################################
def _write_chart_type(self, args):
# Override the virtual superclass method with a chart specific method.
# Write the c:scatterChart element.
self._write_scatter_chart(args)
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_scatter_chart(self, args):
# Write the <c:scatterChart> element.
if args["primary_axes"]:
series = self._get_primary_axes_series()
else:
series = self._get_secondary_axes_series()
if not len(series):
return
style = "lineMarker"
subtype = self.subtype
# Set the user defined chart subtype.
if subtype == "marker_only":
style = "lineMarker"
if subtype == "straight_with_markers":
style = "lineMarker"
if subtype == "straight":
style = "lineMarker"
self.default_marker = {"type": "none"}
if subtype == "smooth_with_markers":
style = "smoothMarker"
if subtype == "smooth":
style = "smoothMarker"
self.default_marker = {"type": "none"}
# Add default formatting to the series data.
self._modify_series_formatting()
self._xml_start_tag("c:scatterChart")
# Write the c:scatterStyle element.
self._write_scatter_style(style)
# Write the series elements.
for data in series:
self._write_ser(data)
# Write the c:axId elements
self._write_axis_ids(args)
self._xml_end_tag("c:scatterChart")
def _write_ser(self, series):
# Over-ridden to write c:xVal/c:yVal instead of c:cat/c:val elements.
# Write the <c:ser> element.
index = self.series_index
self.series_index += 1
self._xml_start_tag("c:ser")
# Write the c:idx element.
self._write_idx(index)
# Write the c:order element.
self._write_order(index)
# Write the series name.
self._write_series_name(series)
# Write the c:spPr element.
self._write_sp_pr(series)
# Write the c:marker element.
self._write_marker(series.get("marker"))
# Write the c:dPt element.
self._write_d_pt(series.get("points"))
# Write the c:dLbls element.
self._write_d_lbls(series.get("labels"))
# Write the c:trendline element.
self._write_trendline(series.get("trendline"))
# Write the c:errBars element.
self._write_error_bars(series.get("error_bars"))
# Write the c:xVal element.
self._write_x_val(series)
# Write the c:yVal element.
self._write_y_val(series)
# Write the c:smooth element.
if "smooth" in self.subtype and series["smooth"] is None:
# Default is on for smooth scatter charts.
self._write_c_smooth(True)
else:
self._write_c_smooth(series["smooth"])
self._xml_end_tag("c:ser")
def _write_plot_area(self):
# Over-ridden to have 2 valAx elements for scatter charts instead
# of catAx/valAx.
#
# Write the <c:plotArea> element.
self._xml_start_tag("c:plotArea")
# Write the c:layout element.
self._write_layout(self.plotarea.get("layout"), "plot")
# Write the subclass chart elements for primary and secondary axes.
self._write_chart_type({"primary_axes": 1})
self._write_chart_type({"primary_axes": 0})
# Write c:catAx and c:valAx elements for series using primary axes.
self._write_cat_val_axis(
{
"x_axis": self.x_axis,
"y_axis": self.y_axis,
"axis_ids": self.axis_ids,
"position": "b",
}
)
tmp = self.horiz_val_axis
self.horiz_val_axis = 1
self._write_val_axis(
{
"x_axis": self.x_axis,
"y_axis": self.y_axis,
"axis_ids": self.axis_ids,
"position": "l",
}
)
self.horiz_val_axis = tmp
# Write c:valAx and c:catAx elements for series using secondary axes
self._write_cat_val_axis(
{
"x_axis": self.x2_axis,
"y_axis": self.y2_axis,
"axis_ids": self.axis2_ids,
"position": "b",
}
)
self.horiz_val_axis = 1
self._write_val_axis(
{
"x_axis": self.x2_axis,
"y_axis": self.y2_axis,
"axis_ids": self.axis2_ids,
"position": "l",
}
)
# Write the c:spPr element for the plotarea formatting.
self._write_sp_pr(self.plotarea)
self._xml_end_tag("c:plotArea")
def _write_x_val(self, series):
# Write the <c:xVal> element.
formula = series.get("categories")
data_id = series.get("cat_data_id")
data = self.formula_data[data_id]
self._xml_start_tag("c:xVal")
# Check the type of cached data.
data_type = self._get_data_type(data)
if data_type == "str":
# Write the c:numRef element.
self._write_str_ref(formula, data, data_type)
else:
# Write the c:numRef element.
self._write_num_ref(formula, data, data_type)
self._xml_end_tag("c:xVal")
def _write_y_val(self, series):
# Write the <c:yVal> element.
formula = series.get("values")
data_id = series.get("val_data_id")
data = self.formula_data[data_id]
self._xml_start_tag("c:yVal")
# Unlike Cat axes data should only be numeric.
# Write the c:numRef element.
self._write_num_ref(formula, data, "num")
self._xml_end_tag("c:yVal")
def _write_scatter_style(self, val):
# Write the <c:scatterStyle> element.
attributes = [("val", val)]
self._xml_empty_tag("c:scatterStyle", attributes)
def _modify_series_formatting(self):
# Add default formatting to the series data unless it has already been
# specified by the user.
subtype = self.subtype
# The default scatter style "markers only" requires a line type.
if subtype == "marker_only":
# Go through each series and define default values.
for series in self.series:
# Set a line type unless there is already a user defined type.
if not series["line"]["defined"]:
series["line"] = {
"width": 2.25,
"none": 1,
"defined": 1,
}
def _write_d_pt_point(self, index, point):
# Write an individual <c:dPt> element. Override the parent method to
# add markers.
self._xml_start_tag("c:dPt")
# Write the c:idx element.
self._write_idx(index)
self._xml_start_tag("c:marker")
# Write the c:spPr element.
self._write_sp_pr(point)
self._xml_end_tag("c:marker")
self._xml_end_tag("c:dPt")

View File

@@ -0,0 +1,124 @@
###############################################################################
#
# ChartStock - A class for writing the Excel XLSX Stock charts.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, 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, options=None):
"""
Constructor.
"""
super(ChartStock, self).__init__()
self.show_crosses = 0
self.hi_low_lines = {}
self.date_category = True
# Override and reset the default axis values.
self.x_axis["defaults"]["num_format"] = "dd/mm/yyyy"
self.x2_axis["defaults"]["num_format"] = "dd/mm/yyyy"
# Set the available data label positions for this chart type.
self.label_position_default = "right"
self.label_positions = {
"center": "ctr",
"right": "r",
"left": "l",
"above": "t",
"below": "b",
# For backward compatibility.
"top": "t",
"bottom": "b",
}
self.set_x_axis({})
self.set_x2_axis({})
###########################################################################
#
# Private API.
#
###########################################################################
def _write_chart_type(self, args):
# Override the virtual superclass method with a chart specific method.
# Write the c:stockChart element.
self._write_stock_chart(args)
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_stock_chart(self, args):
# Write the <c:stockChart> element.
# Overridden to add hi_low_lines().
if args["primary_axes"]:
series = self._get_primary_axes_series()
else:
series = self._get_secondary_axes_series()
if not len(series):
return
# Add default formatting to the series data.
self._modify_series_formatting()
self._xml_start_tag("c:stockChart")
# Write the series elements.
for data in series:
self._write_ser(data)
# Write the c:dropLines element.
self._write_drop_lines()
# Write the c:hiLowLines element.
if args.get("primary_axes"):
self._write_hi_low_lines()
# Write the c:upDownBars element.
self._write_up_down_bars()
# Write the c:axId elements
self._write_axis_ids(args)
self._xml_end_tag("c:stockChart")
def _modify_series_formatting(self):
# Add default formatting to the series data.
index = 0
for series in self.series:
if index % 4 != 3:
if not series["line"]["defined"]:
series["line"] = {"width": 2.25, "none": 1, "defined": 1}
if series["marker"] is None:
if index % 4 == 2:
series["marker"] = {"type": "dot", "size": 3}
else:
series["marker"] = {"type": "none"}
index += 1

View File

@@ -0,0 +1,193 @@
###############################################################################
#
# Chartsheet - A class for writing the Excel XLSX Worksheet file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
from . import worksheet
from .drawing import Drawing
class Chartsheet(worksheet.Worksheet):
"""
A class for writing the Excel XLSX Chartsheet file.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self):
"""
Constructor.
"""
super(Chartsheet, self).__init__()
self.is_chartsheet = True
self.drawing = None
self.chart = None
self.charts = []
self.zoom_scale_normal = 0
self.orientation = 0
self.protection = False
def set_chart(self, chart):
"""
Set the chart object for the chartsheet.
Args:
chart: Chart object.
Returns:
chart: A reference to the chart object.
"""
chart.embedded = False
chart.protection = self.protection
self.chart = chart
self.charts.append([0, 0, chart, 0, 0, 1, 1])
return chart
def protect(self, password="", options=None):
"""
Set the password and protection options of the worksheet.
Args:
password: An optional password string.
options: A dictionary of worksheet objects to protect.
Returns:
Nothing.
"""
# This method is overridden from parent worksheet class.
# Chartsheets only allow a reduced set of protect options.
copy = {}
if not options:
options = {}
if options.get("objects") is None:
copy["objects"] = False
else:
# Objects are default on for chartsheets, so reverse state.
copy["objects"] = not options["objects"]
if options.get("content") is None:
copy["content"] = True
else:
copy["content"] = options["content"]
copy["sheet"] = False
copy["scenarios"] = True
# If objects and content are both off then the chartsheet isn't
# protected, unless it has a password.
if password == "" and copy["objects"] and not copy["content"]:
return
if self.chart:
self.chart.protection = True
else:
self.protection = True
# Call the parent method.
super(Chartsheet, self).protect(password, copy)
###########################################################################
#
# Private API.
#
###########################################################################
def _assemble_xml_file(self):
# Assemble and write the XML file.
# Write the XML declaration.
self._xml_declaration()
# Write the root worksheet element.
self._write_chartsheet()
# Write the worksheet properties.
self._write_sheet_pr()
# Write the sheet view properties.
self._write_sheet_views()
# Write the sheetProtection element.
self._write_sheet_protection()
# Write the printOptions element.
self._write_print_options()
# Write the worksheet page_margins.
self._write_page_margins()
# Write the worksheet page setup.
self._write_page_setup()
# Write the headerFooter element.
self._write_header_footer()
# Write the drawing element.
self._write_drawings()
# Close the worksheet tag.
self._xml_end_tag("chartsheet")
# Close the file.
self._xml_close()
def _prepare_chart(self, index, chart_id, drawing_id):
# Set up chart/drawings.
self.chart.id = chart_id - 1
self.drawing = Drawing()
self.drawing.orientation = self.orientation
self.external_drawing_links.append(
["/drawing", "../drawings/drawing" + str(drawing_id) + ".xml"]
)
self.drawing_links.append(
["/chart", "../charts/chart" + str(chart_id) + ".xml"]
)
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_chartsheet(self):
# Write the <worksheet> element. This is the root element.
schema = "http://schemas.openxmlformats.org/"
xmlns = schema + "spreadsheetml/2006/main"
xmlns_r = schema + "officeDocument/2006/relationships"
attributes = [("xmlns", xmlns), ("xmlns:r", xmlns_r)]
self._xml_start_tag("chartsheet", attributes)
def _write_sheet_pr(self):
# Write the <sheetPr> element for Sheet level properties.
attributes = []
if self.filter_on:
attributes.append(("filterMode", 1))
if self.fit_page or self.tab_color:
self._xml_start_tag("sheetPr", attributes)
self._write_tab_color()
self._write_page_set_up_pr()
self._xml_end_tag("sheetPr")
else:
self._xml_empty_tag("sheetPr", attributes)

View File

@@ -0,0 +1,209 @@
###############################################################################
#
# Comments - A class for writing the Excel XLSX Worksheet file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
from . import xmlwriter
from .utility import preserve_whitespace
from .utility import xl_rowcol_to_cell
class Comments(xmlwriter.XMLwriter):
"""
A class for writing the Excel XLSX Comments file.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self):
"""
Constructor.
"""
super(Comments, self).__init__()
self.author_ids = {}
###########################################################################
#
# Private API.
#
###########################################################################
def _assemble_xml_file(self, comments_data=[]):
# Assemble and write the XML file.
# Write the XML declaration.
self._xml_declaration()
# Write the comments element.
self._write_comments()
# Write the authors element.
self._write_authors(comments_data)
# Write the commentList element.
self._write_comment_list(comments_data)
self._xml_end_tag("comments")
# Close the file.
self._xml_close()
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_comments(self):
# Write the <comments> element.
xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
attributes = [("xmlns", xmlns)]
self._xml_start_tag("comments", attributes)
def _write_authors(self, comment_data):
# Write the <authors> element.
author_count = 0
self._xml_start_tag("authors")
for comment in comment_data:
author = comment[3]
if author is not None and author not in self.author_ids:
# Store the author id.
self.author_ids[author] = author_count
author_count += 1
# Write the author element.
self._write_author(author)
self._xml_end_tag("authors")
def _write_author(self, data):
# Write the <author> element.
self._xml_data_element("author", data)
def _write_comment_list(self, comment_data):
# Write the <commentList> element.
self._xml_start_tag("commentList")
for comment in comment_data:
row = comment[0]
col = comment[1]
text = comment[2]
author = comment[3]
font_name = comment[6]
font_size = comment[7]
font_family = comment[8]
# Look up the author id.
author_id = None
if author is not None:
author_id = self.author_ids[author]
# Write the comment element.
font = (font_name, font_size, font_family)
self._write_comment(row, col, text, author_id, font)
self._xml_end_tag("commentList")
def _write_comment(self, row, col, text, author_id, font):
# Write the <comment> element.
ref = xl_rowcol_to_cell(row, col)
attributes = [("ref", ref)]
if author_id is not None:
attributes.append(("authorId", author_id))
self._xml_start_tag("comment", attributes)
# Write the text element.
self._write_text(text, font)
self._xml_end_tag("comment")
def _write_text(self, text, font):
# Write the <text> element.
self._xml_start_tag("text")
# Write the text r element.
self._write_text_r(text, font)
self._xml_end_tag("text")
def _write_text_r(self, text, font):
# Write the <r> element.
self._xml_start_tag("r")
# Write the rPr element.
self._write_r_pr(font)
# Write the text r element.
self._write_text_t(text)
self._xml_end_tag("r")
def _write_text_t(self, text):
# Write the text <t> element.
attributes = []
if preserve_whitespace(text):
attributes.append(("xml:space", "preserve"))
self._xml_data_element("t", text, attributes)
def _write_r_pr(self, font):
# Write the <rPr> element.
self._xml_start_tag("rPr")
# Write the sz element.
self._write_sz(font[1])
# Write the color element.
self._write_color()
# Write the rFont element.
self._write_r_font(font[0])
# Write the family element.
self._write_family(font[2])
self._xml_end_tag("rPr")
def _write_sz(self, font_size):
# Write the <sz> element.
attributes = [("val", font_size)]
self._xml_empty_tag("sz", attributes)
def _write_color(self):
# Write the <color> element.
attributes = [("indexed", 81)]
self._xml_empty_tag("color", attributes)
def _write_r_font(self, font_name):
# Write the <rFont> element.
attributes = [("val", font_name)]
self._xml_empty_tag("rFont", attributes)
def _write_family(self, font_family):
# Write the <family> element.
attributes = [("val", font_family)]
self._xml_empty_tag("family", attributes)

View File

@@ -0,0 +1,222 @@
###############################################################################
#
# ContentTypes - A class for writing the Excel XLSX ContentTypes file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
import copy
from . import xmlwriter
# Long namespace strings used in the class.
app_package = "application/vnd.openxmlformats-package."
app_document = "application/vnd.openxmlformats-officedocument."
defaults = [
["rels", app_package + "relationships+xml"],
["xml", "application/xml"],
]
overrides = [
["/docProps/app.xml", app_document + "extended-properties+xml"],
["/docProps/core.xml", app_package + "core-properties+xml"],
["/xl/styles.xml", app_document + "spreadsheetml.styles+xml"],
["/xl/theme/theme1.xml", app_document + "theme+xml"],
["/xl/workbook.xml", app_document + "spreadsheetml.sheet.main+xml"],
]
class ContentTypes(xmlwriter.XMLwriter):
"""
A class for writing the Excel XLSX ContentTypes file.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self):
"""
Constructor.
"""
super(ContentTypes, self).__init__()
# Copy the defaults in case we need to change them.
self.defaults = copy.deepcopy(defaults)
self.overrides = copy.deepcopy(overrides)
###########################################################################
#
# Private API.
#
###########################################################################
def _assemble_xml_file(self):
# Assemble and write the XML file.
# Write the XML declaration.
self._xml_declaration()
self._write_types()
self._write_defaults()
self._write_overrides()
self._xml_end_tag("Types")
# Close the file.
self._xml_close()
def _add_default(self, default):
# Add elements to the ContentTypes defaults.
self.defaults.append(default)
def _add_override(self, override):
# Add elements to the ContentTypes overrides.
self.overrides.append(override)
def _add_worksheet_name(self, worksheet_name):
# Add the name of a worksheet to the ContentTypes overrides.
worksheet_name = "/xl/worksheets/" + worksheet_name + ".xml"
self._add_override(
(worksheet_name, app_document + "spreadsheetml.worksheet+xml")
)
def _add_chartsheet_name(self, chartsheet_name):
# Add the name of a chartsheet to the ContentTypes overrides.
chartsheet_name = "/xl/chartsheets/" + chartsheet_name + ".xml"
self._add_override(
(chartsheet_name, app_document + "spreadsheetml.chartsheet+xml")
)
def _add_chart_name(self, chart_name):
# Add the name of a chart to the ContentTypes overrides.
chart_name = "/xl/charts/" + chart_name + ".xml"
self._add_override((chart_name, app_document + "drawingml.chart+xml"))
def _add_drawing_name(self, drawing_name):
# Add the name of a drawing to the ContentTypes overrides.
drawing_name = "/xl/drawings/" + drawing_name + ".xml"
self._add_override((drawing_name, app_document + "drawing+xml"))
def _add_vml_name(self):
# Add the name of a VML drawing to the ContentTypes defaults.
self._add_default(("vml", app_document + "vmlDrawing"))
def _add_comment_name(self, comment_name):
# Add the name of a comment to the ContentTypes overrides.
comment_name = "/xl/" + comment_name + ".xml"
self._add_override((comment_name, app_document + "spreadsheetml.comments+xml"))
def _add_shared_strings(self):
# Add the sharedStrings link to the ContentTypes overrides.
self._add_override(
("/xl/sharedStrings.xml", app_document + "spreadsheetml.sharedStrings+xml")
)
def _add_calc_chain(self):
# Add the calcChain link to the ContentTypes overrides.
self._add_override(
("/xl/calcChain.xml", app_document + "spreadsheetml.calcChain+xml")
)
def _add_image_types(self, image_types):
# Add the image default types.
for image_type in image_types:
extension = image_type
if image_type in ("wmf", "emf"):
image_type = "x-" + image_type
self._add_default((extension, "image/" + image_type))
def _add_table_name(self, table_name):
# Add the name of a table to the ContentTypes overrides.
table_name = "/xl/tables/" + table_name + ".xml"
self._add_override((table_name, app_document + "spreadsheetml.table+xml"))
def _add_vba_project(self):
# Add a vbaProject to the ContentTypes defaults.
# Change the workbook.xml content-type from xlsx to xlsm.
for i, override in enumerate(self.overrides):
if override[0] == "/xl/workbook.xml":
xlsm = "application/vnd.ms-excel.sheet.macroEnabled.main+xml"
self.overrides[i][1] = xlsm
self._add_default(("bin", "application/vnd.ms-office.vbaProject"))
def _add_custom_properties(self):
# Add the custom properties to the ContentTypes overrides.
self._add_override(
("/docProps/custom.xml", app_document + "custom-properties+xml")
)
def _add_metadata(self):
# Add the metadata file to the ContentTypes overrides.
self._add_override(
("/xl/metadata.xml", app_document + "spreadsheetml.sheetMetadata+xml")
)
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_defaults(self):
# Write out all of the <Default> types.
for extension, content_type in self.defaults:
self._xml_empty_tag(
"Default", [("Extension", extension), ("ContentType", content_type)]
)
def _write_overrides(self):
# Write out all of the <Override> types.
for part_name, content_type in self.overrides:
self._xml_empty_tag(
"Override", [("PartName", part_name), ("ContentType", content_type)]
)
def _write_types(self):
# Write the <Types> element.
xmlns = "http://schemas.openxmlformats.org/package/2006/content-types"
attributes = [
(
"xmlns",
xmlns,
)
]
self._xml_start_tag("Types", attributes)
def _write_default(self, extension, content_type):
# Write the <Default> element.
attributes = [
("Extension", extension),
("ContentType", content_type),
]
self._xml_empty_tag("Default", attributes)
def _write_override(self, part_name, content_type):
# Write the <Override> element.
attributes = [
("PartName", part_name),
("ContentType", content_type),
]
self._xml_empty_tag("Override", attributes)

View File

@@ -0,0 +1,205 @@
###############################################################################
#
# Core - A class for writing the Excel XLSX Worksheet file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
# Standard packages.
from datetime import datetime
# Package imports.
from . import xmlwriter
class Core(xmlwriter.XMLwriter):
"""
A class for writing the Excel XLSX Core file.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self):
"""
Constructor.
"""
super(Core, self).__init__()
self.properties = {}
###########################################################################
#
# Private API.
#
###########################################################################
def _assemble_xml_file(self):
# Assemble and write the XML file.
# Write the XML declaration.
self._xml_declaration()
self._write_cp_core_properties()
self._write_dc_title()
self._write_dc_subject()
self._write_dc_creator()
self._write_cp_keywords()
self._write_dc_description()
self._write_cp_last_modified_by()
self._write_dcterms_created()
self._write_dcterms_modified()
self._write_cp_category()
self._write_cp_content_status()
self._xml_end_tag("cp:coreProperties")
# Close the file.
self._xml_close()
def _set_properties(self, properties):
# Set the document properties.
self.properties = properties
def _datetime_to_iso8601_date(self, date):
# Convert to a ISO 8601 style "2010-01-01T00:00:00Z" date.
if not date:
date = datetime.utcnow()
return date.strftime("%Y-%m-%dT%H:%M:%SZ")
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_cp_core_properties(self):
# Write the <cp:coreProperties> element.
xmlns_cp = (
"http://schemas.openxmlformats.org/package/2006/"
+ "metadata/core-properties"
)
xmlns_dc = "http://purl.org/dc/elements/1.1/"
xmlns_dcterms = "http://purl.org/dc/terms/"
xmlns_dcmitype = "http://purl.org/dc/dcmitype/"
xmlns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
attributes = [
("xmlns:cp", xmlns_cp),
("xmlns:dc", xmlns_dc),
("xmlns:dcterms", xmlns_dcterms),
("xmlns:dcmitype", xmlns_dcmitype),
("xmlns:xsi", xmlns_xsi),
]
self._xml_start_tag("cp:coreProperties", attributes)
def _write_dc_creator(self):
# Write the <dc:creator> element.
data = self.properties.get("author", "")
self._xml_data_element("dc:creator", data)
def _write_cp_last_modified_by(self):
# Write the <cp:lastModifiedBy> element.
data = self.properties.get("author", "")
self._xml_data_element("cp:lastModifiedBy", data)
def _write_dcterms_created(self):
# Write the <dcterms:created> element.
date = self.properties.get("created", datetime.utcnow())
xsi_type = "dcterms:W3CDTF"
date = self._datetime_to_iso8601_date(date)
attributes = [
(
"xsi:type",
xsi_type,
)
]
self._xml_data_element("dcterms:created", date, attributes)
def _write_dcterms_modified(self):
# Write the <dcterms:modified> element.
date = self.properties.get("created", datetime.utcnow())
xsi_type = "dcterms:W3CDTF"
date = self._datetime_to_iso8601_date(date)
attributes = [
(
"xsi:type",
xsi_type,
)
]
self._xml_data_element("dcterms:modified", date, attributes)
def _write_dc_title(self):
# Write the <dc:title> element.
if "title" in self.properties:
data = self.properties["title"]
else:
return
self._xml_data_element("dc:title", data)
def _write_dc_subject(self):
# Write the <dc:subject> element.
if "subject" in self.properties:
data = self.properties["subject"]
else:
return
self._xml_data_element("dc:subject", data)
def _write_cp_keywords(self):
# Write the <cp:keywords> element.
if "keywords" in self.properties:
data = self.properties["keywords"]
else:
return
self._xml_data_element("cp:keywords", data)
def _write_dc_description(self):
# Write the <dc:description> element.
if "comments" in self.properties:
data = self.properties["comments"]
else:
return
self._xml_data_element("dc:description", data)
def _write_cp_category(self):
# Write the <cp:category> element.
if "category" in self.properties:
data = self.properties["category"]
else:
return
self._xml_data_element("cp:category", data)
def _write_cp_content_status(self):
# Write the <cp:contentStatus> element.
if "status" in self.properties:
data = self.properties["status"]
else:
return
self._xml_data_element("cp:contentStatus", data)

View File

@@ -0,0 +1,141 @@
###############################################################################
#
# Custom - A class for writing the Excel XLSX Custom Property file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
# Package imports.
from . import xmlwriter
class Custom(xmlwriter.XMLwriter):
"""
A class for writing the Excel XLSX Custom Workbook Property file.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self):
"""
Constructor.
"""
super(Custom, self).__init__()
self.properties = []
self.pid = 1
def _set_properties(self, properties):
# Set the document properties.
self.properties = properties
###########################################################################
#
# Private API.
#
###########################################################################
def _assemble_xml_file(self):
# Assemble and write the XML file.
# Write the XML declaration.
self._xml_declaration()
self._write_properties()
self._xml_end_tag("Properties")
# Close the file.
self._xml_close()
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_properties(self):
# Write the <Properties> element.
schema = "http://schemas.openxmlformats.org/officeDocument/2006/"
xmlns = schema + "custom-properties"
xmlns_vt = schema + "docPropsVTypes"
attributes = [
("xmlns", xmlns),
("xmlns:vt", xmlns_vt),
]
self._xml_start_tag("Properties", attributes)
for custom_property in self.properties:
# Write the property element.
self._write_property(custom_property)
def _write_property(self, custom_property):
# Write the <property> element.
fmtid = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
name, value, property_type = custom_property
self.pid += 1
attributes = [
("fmtid", fmtid),
("pid", self.pid),
("name", name),
]
self._xml_start_tag("property", attributes)
if property_type == "number_int":
# Write the vt:i4 element.
self._write_vt_i4(value)
elif property_type == "number":
# Write the vt:r8 element.
self._write_vt_r8(value)
elif property_type == "date":
# Write the vt:filetime element.
self._write_vt_filetime(value)
elif property_type == "bool":
# Write the vt:bool element.
self._write_vt_bool(value)
else:
# Write the vt:lpwstr element.
self._write_vt_lpwstr(value)
self._xml_end_tag("property")
def _write_vt_lpwstr(self, value):
# Write the <vt:lpwstr> element.
self._xml_data_element("vt:lpwstr", value)
def _write_vt_filetime(self, value):
# Write the <vt:filetime> element.
self._xml_data_element("vt:filetime", value)
def _write_vt_i4(self, value):
# Write the <vt:i4> element.
self._xml_data_element("vt:i4", value)
def _write_vt_r8(self, value):
# Write the <vt:r8> element.
self._xml_data_element("vt:r8", value)
def _write_vt_bool(self, value):
# Write the <vt:bool> element.
if value:
value = "true"
else:
value = "false"
self._xml_data_element("vt:bool", value)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
###############################################################################
#
# Exceptions - A class for XlsxWriter exceptions.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, 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()."""

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,170 @@
###############################################################################
#
# Metadata - A class for writing the Excel XLSX Metadata file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
from . import xmlwriter
class Metadata(xmlwriter.XMLwriter):
"""
A class for writing the Excel XLSX Metadata file.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self):
"""
Constructor.
"""
super(Metadata, self).__init__()
###########################################################################
#
# Private API.
#
###########################################################################
def _assemble_xml_file(self):
# Assemble and write the XML file.
# 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 element.
self._write_future_metadata()
# Write the cellMetadata element.
self._write_cell_metadata()
self._xml_end_tag("metadata")
# Close the file.
self._xml_close()
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_metadata(self):
# Write the <metadata> element.
xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
schema = "http://schemas.microsoft.com/office"
xmlns_xda = schema + "/spreadsheetml/2017/dynamicarray"
attributes = [
("xmlns", xmlns),
("xmlns:xda", xmlns_xda),
]
self._xml_start_tag("metadata", attributes)
def _write_metadata_types(self):
# Write the <metadataTypes> element.
attributes = [("count", 1)]
self._xml_start_tag("metadataTypes", attributes)
# Write the metadataType element.
self._write_metadata_type()
self._xml_end_tag("metadataTypes")
def _write_metadata_type(self):
# Write the <metadataType> element.
attributes = [
("name", "XLDAPR"),
("minSupportedVersion", 120000),
("copy", 1),
("pasteAll", 1),
("pasteValues", 1),
("merge", 1),
("splitFirst", 1),
("rowColShift", 1),
("clearFormats", 1),
("clearComments", 1),
("assign", 1),
("coerce", 1),
("cellMeta", 1),
]
self._xml_empty_tag("metadataType", attributes)
def _write_future_metadata(self):
# Write the <futureMetadata> element.
attributes = [
("name", "XLDAPR"),
("count", 1),
]
self._xml_start_tag("futureMetadata", attributes)
self._xml_start_tag("bk")
self._xml_start_tag("extLst")
# Write the ext element.
self._write_ext()
self._xml_end_tag("extLst")
self._xml_end_tag("bk")
self._xml_end_tag("futureMetadata")
def _write_ext(self):
# Write the <ext> element.
attributes = [("uri", "{bdbb8cdc-fa1e-496e-a857-3c3f30c029c3}")]
self._xml_start_tag("ext", attributes)
# Write the xda:dynamicArrayProperties element.
self._write_xda_dynamic_array_properties()
self._xml_end_tag("ext")
def _write_xda_dynamic_array_properties(self):
# Write the <xda:dynamicArrayProperties> element.
attributes = [
("fDynamic", 1),
("fCollapsed", 0),
]
self._xml_empty_tag("xda:dynamicArrayProperties", attributes)
def _write_cell_metadata(self):
# Write the <cellMetadata> element.
attributes = [("count", 1)]
self._xml_start_tag("cellMetadata", attributes)
self._xml_start_tag("bk")
# Write the rc element.
self._write_rc()
self._xml_end_tag("bk")
self._xml_end_tag("cellMetadata")
def _write_rc(self):
# Write the <rc> element.
attributes = [
("t", 1),
("v", 0),
]
self._xml_empty_tag("rc", attributes)

View File

@@ -0,0 +1,711 @@
###############################################################################
#
# Packager - A class for writing the Excel XLSX Worksheet file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
# Standard packages.
import os
import stat
import tempfile
from shutil import copy
from io import StringIO
from io import BytesIO
# Package imports.
from .app import App
from .contenttypes import ContentTypes
from .core import Core
from .custom import Custom
from .metadata import Metadata
from .relationships import Relationships
from .sharedstrings import SharedStrings
from .styles import Styles
from .theme import Theme
from .vml import Vml
from .table import Table
from .comments import Comments
from .exceptions import EmptyChartSeries
class Packager(object):
"""
A class for writing the Excel XLSX Packager file.
This module is used in conjunction with XlsxWriter to create an
Excel XLSX container file.
From Wikipedia: The Open Packaging Conventions (OPC) is a
container-file technology initially created by Microsoft to store
a combination of XML and non-XML files that together form a single
entity such as an Open XML Paper Specification (OpenXPS)
document. http://en.wikipedia.org/wiki/Open_Packaging_Conventions.
At its simplest an Excel XLSX file contains the following elements::
____ [Content_Types].xml
|
|____ docProps
| |____ app.xml
| |____ core.xml
|
|____ xl
| |____ workbook.xml
| |____ worksheets
| | |____ sheet1.xml
| |
| |____ styles.xml
| |
| |____ theme
| | |____ theme1.xml
| |
| |_____rels
| |____ workbook.xml.rels
|
|_____rels
|____ .rels
The Packager class coordinates the classes that represent the
elements of the package and writes them into the XLSX file.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self):
"""
Constructor.
"""
super(Packager, self).__init__()
self.tmpdir = ""
self.in_memory = False
self.workbook = None
self.worksheet_count = 0
self.chartsheet_count = 0
self.chart_count = 0
self.drawing_count = 0
self.table_count = 0
self.num_vml_files = 0
self.num_comment_files = 0
self.named_ranges = []
self.filenames = []
###########################################################################
#
# Private API.
#
###########################################################################
def _set_tmpdir(self, tmpdir):
# Set an optional user defined temp directory.
self.tmpdir = tmpdir
def _set_in_memory(self, in_memory):
# Set the optional 'in_memory' mode.
self.in_memory = in_memory
def _add_workbook(self, workbook):
# Add the Excel::Writer::XLSX::Workbook object to the package.
self.workbook = workbook
self.chart_count = len(workbook.charts)
self.drawing_count = len(workbook.drawings)
self.num_vml_files = workbook.num_vml_files
self.num_comment_files = workbook.num_comment_files
self.named_ranges = workbook.named_ranges
for worksheet in self.workbook.worksheets():
if worksheet.is_chartsheet:
self.chartsheet_count += 1
else:
self.worksheet_count += 1
def _create_package(self):
# Write the xml files that make up the XLSX OPC package.
self._write_content_types_file()
self._write_root_rels_file()
self._write_workbook_rels_file()
self._write_worksheet_files()
self._write_chartsheet_files()
self._write_workbook_file()
self._write_chart_files()
self._write_drawing_files()
self._write_vml_files()
self._write_comment_files()
self._write_table_files()
self._write_shared_strings_file()
self._write_styles_file()
self._write_custom_file()
self._write_theme_file()
self._write_worksheet_rels_files()
self._write_chartsheet_rels_files()
self._write_drawing_rels_files()
self._add_image_files()
self._add_vba_project()
self._write_core_file()
self._write_app_file()
self._write_metadata_file()
return self.filenames
def _filename(self, xml_filename):
# Create a temp filename to write the XML data to and store the Excel
# filename to use as the name in the Zip container.
if self.in_memory:
os_filename = StringIO()
else:
(fd, os_filename) = tempfile.mkstemp(dir=self.tmpdir)
os.close(fd)
self.filenames.append((os_filename, xml_filename, False))
return os_filename
def _write_workbook_file(self):
# Write the workbook.xml file.
workbook = self.workbook
workbook._set_xml_writer(self._filename("xl/workbook.xml"))
workbook._assemble_xml_file()
def _write_worksheet_files(self):
# Write the worksheet files.
index = 1
for worksheet in self.workbook.worksheets():
if worksheet.is_chartsheet:
continue
if worksheet.constant_memory:
worksheet._opt_reopen()
worksheet._write_single_row()
worksheet._set_xml_writer(
self._filename("xl/worksheets/sheet" + str(index) + ".xml")
)
worksheet._assemble_xml_file()
index += 1
def _write_chartsheet_files(self):
# Write the chartsheet files.
index = 1
for worksheet in self.workbook.worksheets():
if not worksheet.is_chartsheet:
continue
worksheet._set_xml_writer(
self._filename("xl/chartsheets/sheet" + str(index) + ".xml")
)
worksheet._assemble_xml_file()
index += 1
def _write_chart_files(self):
# Write the chart files.
if not self.workbook.charts:
return
index = 1
for chart in self.workbook.charts:
# Check that the chart has at least one data series.
if not chart.series:
raise EmptyChartSeries(
"Chart%d must contain at least one "
"data series. See chart.add_series()." % index
)
chart._set_xml_writer(
self._filename("xl/charts/chart" + str(index) + ".xml")
)
chart._assemble_xml_file()
index += 1
def _write_drawing_files(self):
# Write the drawing files.
if not self.drawing_count:
return
index = 1
for drawing in self.workbook.drawings:
drawing._set_xml_writer(
self._filename("xl/drawings/drawing" + str(index) + ".xml")
)
drawing._assemble_xml_file()
index += 1
def _write_vml_files(self):
# Write the comment VML files.
index = 1
for worksheet in self.workbook.worksheets():
if not worksheet.has_vml and not worksheet.has_header_vml:
continue
if worksheet.has_vml:
vml = Vml()
vml._set_xml_writer(
self._filename("xl/drawings/vmlDrawing" + str(index) + ".vml")
)
vml._assemble_xml_file(
worksheet.vml_data_id,
worksheet.vml_shape_id,
worksheet.comments_list,
worksheet.buttons_list,
)
index += 1
if worksheet.has_header_vml:
vml = Vml()
vml._set_xml_writer(
self._filename("xl/drawings/vmlDrawing" + str(index) + ".vml")
)
vml._assemble_xml_file(
worksheet.vml_header_id,
worksheet.vml_header_id * 1024,
None,
None,
worksheet.header_images_list,
)
self._write_vml_drawing_rels_file(worksheet, index)
index += 1
def _write_comment_files(self):
# Write the comment files.
index = 1
for worksheet in self.workbook.worksheets():
if not worksheet.has_comments:
continue
comment = Comments()
comment._set_xml_writer(self._filename("xl/comments" + str(index) + ".xml"))
comment._assemble_xml_file(worksheet.comments_list)
index += 1
def _write_shared_strings_file(self):
# Write the sharedStrings.xml file.
sst = SharedStrings()
sst.string_table = self.workbook.str_table
if not self.workbook.str_table.count:
return
sst._set_xml_writer(self._filename("xl/sharedStrings.xml"))
sst._assemble_xml_file()
def _write_app_file(self):
# Write the app.xml file.
properties = self.workbook.doc_properties
app = App()
# Add the Worksheet heading pairs.
app._add_heading_pair(["Worksheets", self.worksheet_count])
# Add the Chartsheet heading pairs.
app._add_heading_pair(["Charts", self.chartsheet_count])
# Add the Worksheet parts.
for worksheet in self.workbook.worksheets():
if worksheet.is_chartsheet:
continue
app._add_part_name(worksheet.name)
# Add the Chartsheet parts.
for worksheet in self.workbook.worksheets():
if not worksheet.is_chartsheet:
continue
app._add_part_name(worksheet.name)
# Add the Named Range heading pairs.
if self.named_ranges:
app._add_heading_pair(["Named Ranges", len(self.named_ranges)])
# Add the Named Ranges parts.
for named_range in self.named_ranges:
app._add_part_name(named_range)
app._set_properties(properties)
app.doc_security = self.workbook.read_only
app._set_xml_writer(self._filename("docProps/app.xml"))
app._assemble_xml_file()
def _write_core_file(self):
# Write the core.xml file.
properties = self.workbook.doc_properties
core = Core()
core._set_properties(properties)
core._set_xml_writer(self._filename("docProps/core.xml"))
core._assemble_xml_file()
def _write_metadata_file(self):
# Write the metadata.xml file.
if not self.workbook.has_metadata:
return
metadata = Metadata()
metadata._set_xml_writer(self._filename("xl/metadata.xml"))
metadata._assemble_xml_file()
def _write_custom_file(self):
# Write the custom.xml file.
properties = self.workbook.custom_properties
custom = Custom()
if not len(properties):
return
custom._set_properties(properties)
custom._set_xml_writer(self._filename("docProps/custom.xml"))
custom._assemble_xml_file()
def _write_content_types_file(self):
# Write the ContentTypes.xml file.
content = ContentTypes()
content._add_image_types(self.workbook.image_types)
self._get_table_count()
worksheet_index = 1
chartsheet_index = 1
for worksheet in self.workbook.worksheets():
if worksheet.is_chartsheet:
content._add_chartsheet_name("sheet" + str(chartsheet_index))
chartsheet_index += 1
else:
content._add_worksheet_name("sheet" + str(worksheet_index))
worksheet_index += 1
for i in range(1, self.chart_count + 1):
content._add_chart_name("chart" + str(i))
for i in range(1, self.drawing_count + 1):
content._add_drawing_name("drawing" + str(i))
if self.num_vml_files:
content._add_vml_name()
for i in range(1, self.table_count + 1):
content._add_table_name("table" + str(i))
for i in range(1, self.num_comment_files + 1):
content._add_comment_name("comments" + str(i))
# Add the sharedString rel if there is string data in the workbook.
if self.workbook.str_table.count:
content._add_shared_strings()
# Add vbaProject if present.
if self.workbook.vba_project:
content._add_vba_project()
# 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()
content._set_xml_writer(self._filename("[Content_Types].xml"))
content._assemble_xml_file()
def _write_styles_file(self):
# Write the style xml file.
xf_formats = self.workbook.xf_formats
palette = self.workbook.palette
font_count = self.workbook.font_count
num_formats = self.workbook.num_formats
border_count = self.workbook.border_count
fill_count = self.workbook.fill_count
custom_colors = self.workbook.custom_colors
dxf_formats = self.workbook.dxf_formats
has_comments = self.workbook.has_comments
styles = Styles()
styles._set_style_properties(
[
xf_formats,
palette,
font_count,
num_formats,
border_count,
fill_count,
custom_colors,
dxf_formats,
has_comments,
]
)
styles._set_xml_writer(self._filename("xl/styles.xml"))
styles._assemble_xml_file()
def _write_theme_file(self):
# Write the theme xml file.
theme = Theme()
theme._set_xml_writer(self._filename("xl/theme/theme1.xml"))
theme._assemble_xml_file()
def _write_table_files(self):
# Write the table files.
index = 1
for worksheet in self.workbook.worksheets():
table_props = worksheet.tables
if not table_props:
continue
for table_props in table_props:
table = Table()
table._set_xml_writer(
self._filename("xl/tables/table" + str(index) + ".xml")
)
table._set_properties(table_props)
table._assemble_xml_file()
index += 1
def _get_table_count(self):
# Count the table files. Required for the [Content_Types] file.
for worksheet in self.workbook.worksheets():
for _ in worksheet.tables:
self.table_count += 1
def _write_root_rels_file(self):
# Write the _rels/.rels xml file.
rels = Relationships()
rels._add_document_relationship("/officeDocument", "xl/workbook.xml")
rels._add_package_relationship("/metadata/core-properties", "docProps/core.xml")
rels._add_document_relationship("/extended-properties", "docProps/app.xml")
if self.workbook.custom_properties:
rels._add_document_relationship("/custom-properties", "docProps/custom.xml")
rels._set_xml_writer(self._filename("_rels/.rels"))
rels._assemble_xml_file()
def _write_workbook_rels_file(self):
# Write the _rels/.rels xml file.
rels = Relationships()
worksheet_index = 1
chartsheet_index = 1
for worksheet in self.workbook.worksheets():
if worksheet.is_chartsheet:
rels._add_document_relationship(
"/chartsheet", "chartsheets/sheet" + str(chartsheet_index) + ".xml"
)
chartsheet_index += 1
else:
rels._add_document_relationship(
"/worksheet", "worksheets/sheet" + str(worksheet_index) + ".xml"
)
worksheet_index += 1
rels._add_document_relationship("/theme", "theme/theme1.xml")
rels._add_document_relationship("/styles", "styles.xml")
# Add the sharedString rel if there is string data in the workbook.
if self.workbook.str_table.count:
rels._add_document_relationship("/sharedStrings", "sharedStrings.xml")
# Add vbaProject if present.
if self.workbook.vba_project:
rels._add_ms_package_relationship("/vbaProject", "vbaProject.bin")
# Add the metadata file if required.
if self.workbook.has_metadata:
rels._add_document_relationship("/sheetMetadata", "metadata.xml")
rels._set_xml_writer(self._filename("xl/_rels/workbook.xml.rels"))
rels._assemble_xml_file()
def _write_worksheet_rels_files(self):
# Write data such as hyperlinks or drawings.
index = 0
for worksheet in self.workbook.worksheets():
if worksheet.is_chartsheet:
continue
index += 1
external_links = (
worksheet.external_hyper_links
+ worksheet.external_drawing_links
+ worksheet.external_vml_links
+ worksheet.external_background_links
+ worksheet.external_table_links
+ worksheet.external_comment_links
)
if not external_links:
continue
# Create the worksheet .rels dirs.
rels = Relationships()
for link_data in external_links:
rels._add_document_relationship(*link_data)
# Create .rels file such as /xl/worksheets/_rels/sheet1.xml.rels.
rels._set_xml_writer(
self._filename("xl/worksheets/_rels/sheet" + str(index) + ".xml.rels")
)
rels._assemble_xml_file()
def _write_chartsheet_rels_files(self):
# Write the chartsheet .rels files for links to drawing files.
index = 0
for worksheet in self.workbook.worksheets():
if not worksheet.is_chartsheet:
continue
index += 1
external_links = worksheet.external_drawing_links
if not external_links:
continue
# Create the chartsheet .rels xlsx_dir.
rels = Relationships()
for link_data in external_links:
rels._add_document_relationship(*link_data)
# Create .rels file such as /xl/chartsheets/_rels/sheet1.xml.rels.
rels._set_xml_writer(
self._filename("xl/chartsheets/_rels/sheet" + str(index) + ".xml.rels")
)
rels._assemble_xml_file()
def _write_drawing_rels_files(self):
# Write the drawing .rels files for worksheets with charts or drawings.
index = 0
for worksheet in self.workbook.worksheets():
if worksheet.drawing:
index += 1
if not worksheet.drawing_links:
continue
# Create the drawing .rels xlsx_dir.
rels = Relationships()
for drawing_data in worksheet.drawing_links:
rels._add_document_relationship(*drawing_data)
# Create .rels file such as /xl/drawings/_rels/sheet1.xml.rels.
rels._set_xml_writer(
self._filename("xl/drawings/_rels/drawing" + str(index) + ".xml.rels")
)
rels._assemble_xml_file()
def _write_vml_drawing_rels_file(self, worksheet, index):
# Write the vmlDdrawing .rels files for worksheets with images in
# headers or footers.
# Create the drawing .rels dir.
rels = Relationships()
for drawing_data in worksheet.vml_drawing_links:
rels._add_document_relationship(*drawing_data)
# Create .rels file such as /xl/drawings/_rels/vmlDrawing1.vml.rels.
rels._set_xml_writer(
self._filename("xl/drawings/_rels/vmlDrawing" + str(index) + ".vml.rels")
)
rels._assemble_xml_file()
def _add_image_files(self):
# Write the /xl/media/image?.xml files.
workbook = self.workbook
index = 1
for image in workbook.images:
filename = image[0]
ext = "." + image[1]
image_data = image[2]
xml_image_name = "xl/media/image" + str(index) + ext
if not self.in_memory:
# In file mode we just write or copy the image file.
os_filename = self._filename(xml_image_name)
if image_data:
# The data is in a byte stream. Write it to the target.
os_file = open(os_filename, mode="wb")
os_file.write(image_data.getvalue())
os_file.close()
else:
copy(filename, os_filename)
# Allow copies of Windows read-only images to be deleted.
try:
os.chmod(
os_filename, os.stat(os_filename).st_mode | stat.S_IWRITE
)
except OSError:
pass
else:
# For in-memory mode we read the image into a stream.
if image_data:
# The data is already in a byte stream.
os_filename = image_data
else:
image_file = open(filename, mode="rb")
image_data = image_file.read()
os_filename = BytesIO(image_data)
image_file.close()
self.filenames.append((os_filename, xml_image_name, True))
index += 1
def _add_vba_project(self):
# Copy in a vbaProject.bin file.
vba_project = self.workbook.vba_project
vba_is_stream = self.workbook.vba_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_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_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))

View File

@@ -0,0 +1,115 @@
###############################################################################
#
# Relationships - A class for writing the Excel XLSX Worksheet file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
# Package imports.
from . import xmlwriter
# Long namespace strings used in the class.
schema_root = "http://schemas.openxmlformats.org"
package_schema = schema_root + "/package/2006/relationships"
document_schema = schema_root + "/officeDocument/2006/relationships"
class Relationships(xmlwriter.XMLwriter):
"""
A class for writing the Excel XLSX Relationships file.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self):
"""
Constructor.
"""
super(Relationships, self).__init__()
self.relationships = []
self.id = 1
###########################################################################
#
# Private API.
#
###########################################################################
def _assemble_xml_file(self):
# Assemble and write the XML file.
# Write the XML declaration.
self._xml_declaration()
self._write_relationships()
# Close the file.
self._xml_close()
def _add_document_relationship(self, rel_type, target, target_mode=None):
# Add container relationship to XLSX .rels xml files.
rel_type = document_schema + rel_type
self.relationships.append((rel_type, target, target_mode))
def _add_package_relationship(self, rel_type, target):
# Add container relationship to XLSX .rels xml files.
rel_type = package_schema + rel_type
self.relationships.append((rel_type, target, None))
def _add_ms_package_relationship(self, rel_type, target):
# Add container relationship to XLSX .rels xml files. Uses MS schema.
schema = "http://schemas.microsoft.com/office/2006/relationships"
rel_type = schema + rel_type
self.relationships.append((rel_type, target, None))
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_relationships(self):
# Write the <Relationships> element.
attributes = [
(
"xmlns",
package_schema,
)
]
self._xml_start_tag("Relationships", attributes)
for relationship in self.relationships:
self._write_relationship(relationship)
self._xml_end_tag("Relationships")
def _write_relationship(self, relationship):
# Write the <Relationship> element.
rel_type, target, target_mode = relationship
attributes = [
("Id", "rId" + str(self.id)),
("Type", rel_type),
("Target", target),
]
self.id += 1
if target_mode:
attributes.append(("TargetMode", target_mode))
self._xml_empty_tag("Relationship", attributes)

View File

@@ -0,0 +1,414 @@
###############################################################################
#
# Shape - A class for to represent Excel XLSX shape objects.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
import copy
from warnings import warn
class Shape(object):
"""
A class for to represent Excel XLSX shape objects.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self, shape_type, name, options):
"""
Constructor.
"""
super(Shape, self).__init__()
self.name = name
self.shape_type = shape_type
self.connect = 0
self.drawing = 0
self.edit_as = ""
self.id = 0
self.text = ""
self.textlink = ""
self.stencil = 1
self.element = -1
self.start = None
self.start_index = None
self.end = None
self.end_index = None
self.adjustments = []
self.start_side = ""
self.end_side = ""
self.flip_h = 0
self.flip_v = 0
self.rotation = 0
self.text_rotation = 0
self.textbox = False
self.align = None
self.fill = None
self.font = None
self.format = None
self.line = None
self.url_rel_index = None
self.tip = None
self._set_options(options)
###########################################################################
#
# Private API.
#
###########################################################################
def _set_options(self, options):
self.align = self._get_align_properties(options.get("align"))
self.fill = self._get_fill_properties(options.get("fill"))
self.font = self._get_font_properties(options.get("font"))
self.gradient = self._get_gradient_properties(options.get("gradient"))
self.line = self._get_line_properties(options.get("line"))
self.text_rotation = options.get("text_rotation", 0)
self.textlink = options.get("textlink", "")
if self.textlink.startswith("="):
self.textlink = self.textlink.lstrip("=")
if options.get("border"):
self.line = self._get_line_properties(options["border"])
# Gradient fill overrides solid fill.
if self.gradient:
self.fill = None
###########################################################################
#
# Static methods for processing chart/shape style properties.
#
###########################################################################
@staticmethod
def _get_line_properties(line):
# Convert user line properties to the structure required internally.
if not line:
return {"defined": False}
# Copy the user defined properties since they will be modified.
line = copy.deepcopy(line)
dash_types = {
"solid": "solid",
"round_dot": "sysDot",
"square_dot": "sysDash",
"dash": "dash",
"dash_dot": "dashDot",
"long_dash": "lgDash",
"long_dash_dot": "lgDashDot",
"long_dash_dot_dot": "lgDashDotDot",
"dot": "dot",
"system_dash_dot": "sysDashDot",
"system_dash_dot_dot": "sysDashDotDot",
}
# Check the dash type.
dash_type = line.get("dash_type")
if dash_type is not None:
if dash_type in dash_types:
line["dash_type"] = dash_types[dash_type]
else:
warn("Unknown dash type '%s'" % dash_type)
return
line["defined"] = True
return line
@staticmethod
def _get_fill_properties(fill):
# Convert user fill properties to the structure required internally.
if not fill:
return {"defined": False}
# Copy the user defined properties since they will be modified.
fill = copy.deepcopy(fill)
fill["defined"] = True
return fill
@staticmethod
def _get_pattern_properties(pattern):
# Convert user defined pattern to the structure required internally.
if not pattern:
return
# Copy the user defined properties since they will be modified.
pattern = copy.deepcopy(pattern)
if not pattern.get("pattern"):
warn("Pattern must include 'pattern'")
return
if not pattern.get("fg_color"):
warn("Pattern must include 'fg_color'")
return
types = {
"percent_5": "pct5",
"percent_10": "pct10",
"percent_20": "pct20",
"percent_25": "pct25",
"percent_30": "pct30",
"percent_40": "pct40",
"percent_50": "pct50",
"percent_60": "pct60",
"percent_70": "pct70",
"percent_75": "pct75",
"percent_80": "pct80",
"percent_90": "pct90",
"light_downward_diagonal": "ltDnDiag",
"light_upward_diagonal": "ltUpDiag",
"dark_downward_diagonal": "dkDnDiag",
"dark_upward_diagonal": "dkUpDiag",
"wide_downward_diagonal": "wdDnDiag",
"wide_upward_diagonal": "wdUpDiag",
"light_vertical": "ltVert",
"light_horizontal": "ltHorz",
"narrow_vertical": "narVert",
"narrow_horizontal": "narHorz",
"dark_vertical": "dkVert",
"dark_horizontal": "dkHorz",
"dashed_downward_diagonal": "dashDnDiag",
"dashed_upward_diagonal": "dashUpDiag",
"dashed_horizontal": "dashHorz",
"dashed_vertical": "dashVert",
"small_confetti": "smConfetti",
"large_confetti": "lgConfetti",
"zigzag": "zigZag",
"wave": "wave",
"diagonal_brick": "diagBrick",
"horizontal_brick": "horzBrick",
"weave": "weave",
"plaid": "plaid",
"divot": "divot",
"dotted_grid": "dotGrid",
"dotted_diamond": "dotDmnd",
"shingle": "shingle",
"trellis": "trellis",
"sphere": "sphere",
"small_grid": "smGrid",
"large_grid": "lgGrid",
"small_check": "smCheck",
"large_check": "lgCheck",
"outlined_diamond": "openDmnd",
"solid_diamond": "solidDmnd",
}
# Check for valid types.
if pattern["pattern"] not in types:
warn("unknown pattern type '%s'" % pattern["pattern"])
return
else:
pattern["pattern"] = types[pattern["pattern"]]
# Specify a default background color.
pattern["bg_color"] = pattern.get("bg_color", "#FFFFFF")
return pattern
@staticmethod
def _get_gradient_properties(gradient):
# 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 type(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("Unknown gradient type '%s" % gradient_type)
return
else:
gradient["type"] = "linear"
return gradient
@staticmethod
def _get_font_properties(options):
# Convert user defined font values into private dict values.
if options is None:
options = {}
font = {
"name": options.get("name"),
"color": options.get("color"),
"size": options.get("size", 11),
"bold": options.get("bold"),
"italic": options.get("italic"),
"underline": options.get("underline"),
"pitch_family": options.get("pitch_family"),
"charset": options.get("charset"),
"baseline": options.get("baseline", -1),
"lang": options.get("lang", "en-US"),
}
# Convert font size units.
if font["size"]:
font["size"] = int(font["size"] * 100)
return font
@staticmethod
def _get_font_style_attributes(font):
# _get_font_style_attributes.
attributes = []
if not font:
return attributes
if font.get("size"):
attributes.append(("sz", font["size"]))
if font.get("bold") is not None:
attributes.append(("b", 0 + font["bold"]))
if font.get("italic") is not None:
attributes.append(("i", 0 + font["italic"]))
if font.get("underline") is not None:
attributes.append(("u", "sng"))
if font.get("baseline") != -1:
attributes.append(("baseline", font["baseline"]))
return attributes
@staticmethod
def _get_font_latin_attributes(font):
# _get_font_latin_attributes.
attributes = []
if not font:
return attributes
if font["name"] is not None:
attributes.append(("typeface", font["name"]))
if font["pitch_family"] is not None:
attributes.append(("pitchFamily", font["pitch_family"]))
if font["charset"] is not None:
attributes.append(("charset", font["charset"]))
return attributes
@staticmethod
def _get_align_properties(align):
# Convert user defined align to the structure required internally.
if not align:
return {"defined": False}
# Copy the user defined properties since they will be modified.
align = copy.deepcopy(align)
if "vertical" in align:
align_type = align["vertical"]
align_types = {
"top": "top",
"middle": "middle",
"bottom": "bottom",
}
if align_type in align_types:
align["vertical"] = align_types[align_type]
else:
warn("Unknown alignment type '%s'" % 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("Unknown alignment type '%s'" % align_type)
return {"defined": False}
align["defined"] = True
return align

View File

@@ -0,0 +1,158 @@
###############################################################################
#
# SharedStrings - A class for writing the Excel XLSX sharedStrings file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
# Standard packages.
import re
# Package imports.
from . import xmlwriter
from .utility import preserve_whitespace
# 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])")
class SharedStrings(xmlwriter.XMLwriter):
"""
A class for writing the Excel XLSX sharedStrings file.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self):
"""
Constructor.
"""
super(SharedStrings, self).__init__()
self.string_table = None
###########################################################################
#
# Private API.
#
###########################################################################
def _assemble_xml_file(self):
# Assemble and write the XML file.
# Write the XML declaration.
self._xml_declaration()
# Write the sst element.
self._write_sst()
# Write the sst strings.
self._write_sst_strings()
# Close the sst tag.
self._xml_end_tag("sst")
# Close the file.
self._xml_close()
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_sst(self):
# Write the <sst> element.
xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
attributes = [
("xmlns", xmlns),
("count", self.string_table.count),
("uniqueCount", self.string_table.unique_count),
]
self._xml_start_tag("sst", attributes)
def _write_sst_strings(self):
# Write the sst string elements.
for string in self.string_table.string_array:
self._write_si(string)
def _write_si(self, string):
# Write the <si> element.
attributes = []
# 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.
# Escape the escape.
string = re_control_chars_1.sub(r"_x005F\1", string)
# Convert control character to the _xHHHH_ escape.
string = re_control_chars_2.sub(
lambda match: "_x%04X_" % ord(match.group(1)), string
)
# Escapes non characters in strings.
string = string.replace("\uFFFE", "_xFFFE_")
string = string.replace("\uFFFF", "_xFFFF_")
# 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(object):
"""
A class to track Excel shared strings between worksheets.
"""
def __init__(self):
self.count = 0
self.unique_count = 0
self.string_table = {}
self.string_array = []
def _get_shared_string_index(self, string):
""" " Get the index of the string in the Shared String table."""
if string not in self.string_table:
# String isn't already stored in the table so add it.
index = self.unique_count
self.string_table[string] = index
self.count += 1
self.unique_count += 1
return index
else:
# String exists in the table.
index = self.string_table[string]
self.count += 1
return index
def _get_shared_string(self, index):
""" " Get a shared string from the index."""
return self.string_array[index]
def _sort_string_data(self):
""" " Sort the shared string data and convert from dict to list."""
self.string_array = sorted(self.string_table, key=self.string_table.__getitem__)
self.string_table = {}

View File

@@ -0,0 +1,754 @@
###############################################################################
#
# Styles - A class for writing the Excel XLSX Worksheet file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
# Package imports.
from . import xmlwriter
class Styles(xmlwriter.XMLwriter):
"""
A class for writing the Excel XLSX Styles file.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self):
"""
Constructor.
"""
super(Styles, self).__init__()
self.xf_formats = []
self.palette = []
self.font_count = 0
self.num_formats = []
self.border_count = 0
self.fill_count = 0
self.custom_colors = []
self.dxf_formats = []
self.has_hyperlink = False
self.hyperlink_font_id = 0
self.has_comments = False
###########################################################################
#
# Private API.
#
###########################################################################
def _assemble_xml_file(self):
# Assemble and write the XML file.
# Write the XML declaration.
self._xml_declaration()
# Add the style sheet.
self._write_style_sheet()
# Write the number formats.
self._write_num_fmts()
# Write the fonts.
self._write_fonts()
# Write the fills.
self._write_fills()
# Write the borders element.
self._write_borders()
# Write the cellStyleXfs element.
self._write_cell_style_xfs()
# Write the cellXfs element.
self._write_cell_xfs()
# Write the cellStyles element.
self._write_cell_styles()
# Write the dxfs element.
self._write_dxfs()
# Write the tableStyles element.
self._write_table_styles()
# Write the colors element.
self._write_colors()
# Close the style sheet tag.
self._xml_end_tag("styleSheet")
# Close the file.
self._xml_close()
def _set_style_properties(self, properties):
# Pass in the Format objects and other properties used in the styles.
self.xf_formats = properties[0]
self.palette = properties[1]
self.font_count = properties[2]
self.num_formats = properties[3]
self.border_count = properties[4]
self.fill_count = properties[5]
self.custom_colors = properties[6]
self.dxf_formats = properties[7]
self.has_comments = properties[8]
def _get_palette_color(self, color):
# Special handling for automatic color.
if color == "Automatic":
return color
# Convert the RGB color.
if color[0] == "#":
color = color[1:]
return "FF" + color.upper()
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_style_sheet(self):
# Write the <styleSheet> element.
xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
attributes = [("xmlns", xmlns)]
self._xml_start_tag("styleSheet", attributes)
def _write_num_fmts(self):
# Write the <numFmts> element.
if not self.num_formats:
return
attributes = [("count", len(self.num_formats))]
self._xml_start_tag("numFmts", attributes)
# Write the numFmts elements.
for index, num_format in enumerate(self.num_formats, 164):
self._write_num_fmt(index, num_format)
self._xml_end_tag("numFmts")
def _write_num_fmt(self, num_fmt_id, format_code):
# Write the <numFmt> element.
format_codes = {
0: "General",
1: "0",
2: "0.00",
3: "#,##0",
4: "#,##0.00",
5: "($#,##0_);($#,##0)",
6: "($#,##0_);[Red]($#,##0)",
7: "($#,##0.00_);($#,##0.00)",
8: "($#,##0.00_);[Red]($#,##0.00)",
9: "0%",
10: "0.00%",
11: "0.00E+00",
12: "# ?/?",
13: "# ??/??",
14: "m/d/yy",
15: "d-mmm-yy",
16: "d-mmm",
17: "mmm-yy",
18: "h:mm AM/PM",
19: "h:mm:ss AM/PM",
20: "h:mm",
21: "h:mm:ss",
22: "m/d/yy h:mm",
37: "(#,##0_);(#,##0)",
38: "(#,##0_);[Red](#,##0)",
39: "(#,##0.00_);(#,##0.00)",
40: "(#,##0.00_);[Red](#,##0.00)",
41: '_(* #,##0_);_(* (#,##0);_(* "-"_);_(_)',
42: '_($* #,##0_);_($* (#,##0);_($* "-"_);_(_)',
43: '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(_)',
44: '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(_)',
45: "mm:ss",
46: "[h]:mm:ss",
47: "mm:ss.0",
48: "##0.0E+0",
49: "@",
}
# Set the format code for built-in number formats.
if num_fmt_id < 164:
if num_fmt_id in format_codes:
format_code = format_codes[num_fmt_id]
else:
format_code = "General"
attributes = [
("numFmtId", num_fmt_id),
("formatCode", format_code),
]
self._xml_empty_tag("numFmt", attributes)
def _write_fonts(self):
# Write the <fonts> element.
if self.has_comments:
# Add extra font for comments.
attributes = [("count", self.font_count + 1)]
else:
attributes = [("count", self.font_count)]
self._xml_start_tag("fonts", attributes)
# Write the font elements for xf_format objects that have them.
for xf_format in self.xf_formats:
if xf_format.has_font:
self._write_font(xf_format)
if self.has_comments:
self._write_comment_font()
self._xml_end_tag("fonts")
def _write_font(self, xf_format, is_dxf_format=False):
# Write the <font> element.
self._xml_start_tag("font")
# The condense and extend elements are mainly used in dxf formats.
if xf_format.font_condense:
self._write_condense()
if xf_format.font_extend:
self._write_extend()
if xf_format.bold:
self._xml_empty_tag("b")
if xf_format.italic:
self._xml_empty_tag("i")
if xf_format.font_strikeout:
self._xml_empty_tag("strike")
if xf_format.font_outline:
self._xml_empty_tag("outline")
if xf_format.font_shadow:
self._xml_empty_tag("shadow")
# Handle the underline variants.
if xf_format.underline:
self._write_underline(xf_format.underline)
if xf_format.font_script == 1:
self._write_vert_align("superscript")
if xf_format.font_script == 2:
self._write_vert_align("subscript")
if not is_dxf_format:
self._xml_empty_tag("sz", [("val", xf_format.font_size)])
if xf_format.theme == -1:
# Ignore for excel2003_style.
pass
elif xf_format.theme:
self._write_color("theme", xf_format.theme)
elif xf_format.color_indexed:
self._write_color("indexed", xf_format.color_indexed)
elif xf_format.font_color:
color = self._get_palette_color(xf_format.font_color)
if color != "Automatic":
self._write_color("rgb", color)
elif not is_dxf_format:
self._write_color("theme", 1)
if not is_dxf_format:
self._xml_empty_tag("name", [("val", xf_format.font_name)])
if xf_format.font_family:
self._xml_empty_tag("family", [("val", xf_format.font_family)])
if xf_format.font_charset:
self._xml_empty_tag("charset", [("val", xf_format.font_charset)])
if xf_format.font_name == "Calibri" and not xf_format.hyperlink:
self._xml_empty_tag("scheme", [("val", xf_format.font_scheme)])
if xf_format.hyperlink:
self.has_hyperlink = True
if self.hyperlink_font_id == 0:
self.hyperlink_font_id = xf_format.font_index
self._xml_end_tag("font")
def _write_comment_font(self):
# Write the <font> element for comments.
self._xml_start_tag("font")
self._xml_empty_tag("sz", [("val", 8)])
self._write_color("indexed", 81)
self._xml_empty_tag("name", [("val", "Tahoma")])
self._xml_empty_tag("family", [("val", 2)])
self._xml_end_tag("font")
def _write_underline(self, underline):
# Write the underline font element.
if underline == 2:
attributes = [("val", "double")]
elif underline == 33:
attributes = [("val", "singleAccounting")]
elif underline == 34:
attributes = [("val", "doubleAccounting")]
else:
# Default to single underline.
attributes = []
self._xml_empty_tag("u", attributes)
def _write_vert_align(self, val):
# Write the <vertAlign> font sub-element.
attributes = [("val", val)]
self._xml_empty_tag("vertAlign", attributes)
def _write_color(self, name, value):
# Write the <color> element.
attributes = [(name, value)]
self._xml_empty_tag("color", attributes)
def _write_fills(self):
# Write the <fills> element.
attributes = [("count", self.fill_count)]
self._xml_start_tag("fills", attributes)
# Write the default fill element.
self._write_default_fill("none")
self._write_default_fill("gray125")
# Write the fill elements for xf_format objects that have them.
for xf_format in self.xf_formats:
if xf_format.has_fill:
self._write_fill(xf_format)
self._xml_end_tag("fills")
def _write_default_fill(self, pattern_type):
# Write the <fill> element for the default fills.
self._xml_start_tag("fill")
self._xml_empty_tag("patternFill", [("patternType", pattern_type)])
self._xml_end_tag("fill")
def _write_fill(self, xf_format, is_dxf_format=False):
# Write the <fill> element.
pattern = xf_format.pattern
bg_color = xf_format.bg_color
fg_color = xf_format.fg_color
# Colors for dxf formats are handled differently from normal formats
# since the normal xf_format reverses the meaning of BG and FG for
# solid fills.
if is_dxf_format:
bg_color = xf_format.dxf_bg_color
fg_color = xf_format.dxf_fg_color
patterns = (
"none",
"solid",
"mediumGray",
"darkGray",
"lightGray",
"darkHorizontal",
"darkVertical",
"darkDown",
"darkUp",
"darkGrid",
"darkTrellis",
"lightHorizontal",
"lightVertical",
"lightDown",
"lightUp",
"lightGrid",
"lightTrellis",
"gray125",
"gray0625",
)
# Special handling for pattern only case.
if not fg_color and not bg_color and patterns[pattern]:
self._write_default_fill(patterns[pattern])
return
self._xml_start_tag("fill")
# The "none" pattern is handled differently for dxf formats.
if is_dxf_format and pattern <= 1:
self._xml_start_tag("patternFill")
else:
self._xml_start_tag("patternFill", [("patternType", patterns[pattern])])
if fg_color:
fg_color = self._get_palette_color(fg_color)
if fg_color != "Automatic":
self._xml_empty_tag("fgColor", [("rgb", fg_color)])
if bg_color:
bg_color = self._get_palette_color(bg_color)
if bg_color != "Automatic":
self._xml_empty_tag("bgColor", [("rgb", bg_color)])
else:
if not is_dxf_format and pattern <= 1:
self._xml_empty_tag("bgColor", [("indexed", 64)])
self._xml_end_tag("patternFill")
self._xml_end_tag("fill")
def _write_borders(self):
# Write the <borders> element.
attributes = [("count", self.border_count)]
self._xml_start_tag("borders", attributes)
# Write the border elements for xf_format objects that have them.
for xf_format in self.xf_formats:
if xf_format.has_border:
self._write_border(xf_format)
self._xml_end_tag("borders")
def _write_border(self, xf_format, is_dxf_format=False):
# Write the <border> element.
attributes = []
# Diagonal borders add attributes to the <border> element.
if xf_format.diag_type == 1:
attributes.append(("diagonalUp", 1))
elif xf_format.diag_type == 2:
attributes.append(("diagonalDown", 1))
elif xf_format.diag_type == 3:
attributes.append(("diagonalUp", 1))
attributes.append(("diagonalDown", 1))
# Ensure that a default diag border is set if the diag type is set.
if xf_format.diag_type and not xf_format.diag_border:
xf_format.diag_border = 1
# Write the start border tag.
self._xml_start_tag("border", attributes)
# Write the <border> sub elements.
self._write_sub_border("left", xf_format.left, xf_format.left_color)
self._write_sub_border("right", xf_format.right, xf_format.right_color)
self._write_sub_border("top", xf_format.top, xf_format.top_color)
self._write_sub_border("bottom", xf_format.bottom, xf_format.bottom_color)
# Condition DXF formats don't allow diagonal borders.
if not is_dxf_format:
self._write_sub_border(
"diagonal", xf_format.diag_border, xf_format.diag_color
)
if is_dxf_format:
self._write_sub_border("vertical", None, None)
self._write_sub_border("horizontal", None, None)
self._xml_end_tag("border")
def _write_sub_border(self, border_type, style, color):
# Write the <border> sub elements such as <right>, <top>, etc.
attributes = []
if not style:
self._xml_empty_tag(border_type)
return
border_styles = (
"none",
"thin",
"medium",
"dashed",
"dotted",
"thick",
"double",
"hair",
"mediumDashed",
"dashDot",
"mediumDashDot",
"dashDotDot",
"mediumDashDotDot",
"slantDashDot",
)
attributes.append(("style", border_styles[style]))
self._xml_start_tag(border_type, attributes)
if color and color != "Automatic":
color = self._get_palette_color(color)
self._xml_empty_tag("color", [("rgb", color)])
else:
self._xml_empty_tag("color", [("auto", 1)])
self._xml_end_tag(border_type)
def _write_cell_style_xfs(self):
# Write the <cellStyleXfs> element.
count = 1
if self.has_hyperlink:
count = 2
attributes = [("count", count)]
self._xml_start_tag("cellStyleXfs", attributes)
self._write_style_xf()
if self.has_hyperlink:
self._write_style_xf(True, self.hyperlink_font_id)
self._xml_end_tag("cellStyleXfs")
def _write_cell_xfs(self):
# Write the <cellXfs> element.
formats = self.xf_formats
# Workaround for when the last xf_format is used for the comment font
# and shouldn't be used for cellXfs.
last_format = formats[-1]
if last_format.font_only:
formats.pop()
attributes = [("count", len(formats))]
self._xml_start_tag("cellXfs", attributes)
# Write the xf elements.
for xf_format in formats:
self._write_xf(xf_format)
self._xml_end_tag("cellXfs")
def _write_style_xf(self, has_hyperlink=False, font_id=0):
# Write the style <xf> element.
num_fmt_id = 0
fill_id = 0
border_id = 0
attributes = [
("numFmtId", num_fmt_id),
("fontId", font_id),
("fillId", fill_id),
("borderId", border_id),
]
if has_hyperlink:
attributes.append(("applyNumberFormat", 0))
attributes.append(("applyFill", 0))
attributes.append(("applyBorder", 0))
attributes.append(("applyAlignment", 0))
attributes.append(("applyProtection", 0))
self._xml_start_tag("xf", attributes)
self._xml_empty_tag("alignment", [("vertical", "top")])
self._xml_empty_tag("protection", [("locked", 0)])
self._xml_end_tag("xf")
else:
self._xml_empty_tag("xf", attributes)
def _write_xf(self, xf_format):
# Write the <xf> element.
num_fmt_id = xf_format.num_format_index
font_id = xf_format.font_index
fill_id = xf_format.fill_index
border_id = xf_format.border_index
xf_id = xf_format.xf_id
has_align = 0
has_protect = 0
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_align = 1
# 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_protect = 1
# Write XF with sub-elements if required.
if has_align or has_protect:
self._xml_start_tag("xf", attributes)
if has_align:
self._xml_empty_tag("alignment", align)
if has_protect:
self._xml_empty_tag("protection", protection)
self._xml_end_tag("xf")
else:
self._xml_empty_tag("xf", attributes)
def _write_cell_styles(self):
# Write the <cellStyles> element.
count = 1
if self.has_hyperlink:
count = 2
attributes = [("count", count)]
self._xml_start_tag("cellStyles", attributes)
if self.has_hyperlink:
self._write_cell_style("Hyperlink", 1, 8)
self._write_cell_style()
self._xml_end_tag("cellStyles")
def _write_cell_style(self, name="Normal", xf_id=0, builtin_id=0):
# Write the <cellStyle> element.
attributes = [
("name", name),
("xfId", xf_id),
("builtinId", builtin_id),
]
self._xml_empty_tag("cellStyle", attributes)
def _write_dxfs(self):
# Write the <dxfs> element.
formats = self.dxf_formats
count = len(formats)
attributes = [("count", len(formats))]
if count:
self._xml_start_tag("dxfs", attributes)
# Write the font elements for xf_format objects that have them.
for xf_format in self.dxf_formats:
self._xml_start_tag("dxf")
if xf_format.has_dxf_font:
self._write_font(xf_format, True)
if xf_format.num_format_index:
self._write_num_fmt(
xf_format.num_format_index, xf_format.num_format
)
if xf_format.has_dxf_fill:
self._write_fill(xf_format, True)
if xf_format.has_dxf_border:
self._write_border(xf_format, True)
self._xml_end_tag("dxf")
self._xml_end_tag("dxfs")
else:
self._xml_empty_tag("dxfs", attributes)
def _write_table_styles(self):
# Write the <tableStyles> element.
count = 0
default_table_style = "TableStyleMedium9"
default_pivot_style = "PivotStyleLight16"
attributes = [
("count", count),
("defaultTableStyle", default_table_style),
("defaultPivotStyle", default_pivot_style),
]
self._xml_empty_tag("tableStyles", attributes)
def _write_colors(self):
# Write the <colors> element.
custom_colors = self.custom_colors
if not custom_colors:
return
self._xml_start_tag("colors")
self._write_mru_colors(custom_colors)
self._xml_end_tag("colors")
def _write_mru_colors(self, custom_colors):
# Write the <mruColors> element for the most recently used colors.
# Write the custom custom_colors in reverse order.
custom_colors.reverse()
# Limit the mruColors to the last 10.
if len(custom_colors) > 10:
custom_colors = custom_colors[0:10]
self._xml_start_tag("mruColors")
# Write the custom custom_colors in reverse order.
for color in custom_colors:
self._write_color("rgb", color)
self._xml_end_tag("mruColors")
def _write_condense(self):
# Write the <condense> element.
attributes = [("val", 0)]
self._xml_empty_tag("condense", attributes)
def _write_extend(self):
# Write the <extend> element.
attributes = [("val", 0)]
self._xml_empty_tag("extend", attributes)

View File

@@ -0,0 +1,184 @@
###############################################################################
#
# Table - A class for writing the Excel XLSX Worksheet file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
from . import xmlwriter
class Table(xmlwriter.XMLwriter):
"""
A class for writing the Excel XLSX Table file.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self):
"""
Constructor.
"""
super(Table, self).__init__()
self.properties = {}
###########################################################################
#
# Private API.
#
###########################################################################
def _assemble_xml_file(self):
# Assemble and write the XML file.
# Write the XML declaration.
self._xml_declaration()
# Write the table element.
self._write_table()
# Write the autoFilter element.
self._write_auto_filter()
# Write the tableColumns element.
self._write_table_columns()
# Write the tableStyleInfo element.
self._write_table_style_info()
# Close the table tag.
self._xml_end_tag("table")
# Close the file.
self._xml_close()
def _set_properties(self, properties):
# Set the document properties.
self.properties = properties
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_table(self):
# Write the <table> element.
schema = "http://schemas.openxmlformats.org/"
xmlns = schema + "spreadsheetml/2006/main"
table_id = self.properties["id"]
name = self.properties["name"]
display_name = self.properties["name"]
ref = self.properties["range"]
totals_row_shown = self.properties["totals_row_shown"]
header_row_count = self.properties["header_row_count"]
attributes = [
("xmlns", xmlns),
("id", table_id),
("name", name),
("displayName", display_name),
("ref", ref),
]
if not header_row_count:
attributes.append(("headerRowCount", 0))
if totals_row_shown:
attributes.append(("totalsRowCount", 1))
else:
attributes.append(("totalsRowShown", 0))
self._xml_start_tag("table", attributes)
def _write_auto_filter(self):
# Write the <autoFilter> element.
autofilter = self.properties.get("autofilter", 0)
if not autofilter:
return
attributes = [
(
"ref",
autofilter,
)
]
self._xml_empty_tag("autoFilter", attributes)
def _write_table_columns(self):
# Write the <tableColumns> element.
columns = self.properties["columns"]
count = len(columns)
attributes = [("count", count)]
self._xml_start_tag("tableColumns", attributes)
for col_data in columns:
# Write the tableColumn element.
self._write_table_column(col_data)
self._xml_end_tag("tableColumns")
def _write_table_column(self, col_data):
# Write the <tableColumn> element.
attributes = [
("id", col_data["id"]),
("name", col_data["name"]),
]
if col_data.get("total_string"):
attributes.append(("totalsRowLabel", col_data["total_string"]))
elif col_data.get("total_function"):
attributes.append(("totalsRowFunction", col_data["total_function"]))
if "format" in col_data and col_data["format"] is not None:
attributes.append(("dataDxfId", col_data["format"]))
if col_data.get("formula"):
self._xml_start_tag("tableColumn", attributes)
# Write the calculatedColumnFormula element.
self._write_calculated_column_formula(col_data["formula"])
self._xml_end_tag("tableColumn")
else:
self._xml_empty_tag("tableColumn", attributes)
def _write_table_style_info(self):
# Write the <tableStyleInfo> element.
props = self.properties
attributes = []
name = props["style"]
show_first_column = 0 + props["show_first_col"]
show_last_column = 0 + props["show_last_col"]
show_row_stripes = 0 + props["show_row_stripes"]
show_column_stripes = 0 + props["show_col_stripes"]
if name is not None and name != "" and name != "None":
attributes.append(("name", name))
attributes.append(("showFirstColumn", show_first_column))
attributes.append(("showLastColumn", show_last_column))
attributes.append(("showRowStripes", show_row_stripes))
attributes.append(("showColumnStripes", show_column_stripes))
self._xml_empty_tag("tableStyleInfo", attributes)
def _write_calculated_column_formula(self, formula):
# Write the <calculatedColumnFormula> element.
self._xml_data_element("calculatedColumnFormula", formula)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,875 @@
###############################################################################
#
# Worksheet - A class for writing Excel Worksheets.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
import re
import datetime
from warnings import warn
COL_NAMES = {}
CHAR_WIDTHS = {
" ": 3,
"!": 5,
'"': 6,
"#": 7,
"$": 7,
"%": 11,
"&": 10,
"'": 3,
"(": 5,
")": 5,
"*": 7,
"+": 7,
",": 4,
"-": 5,
".": 4,
"/": 6,
"0": 7,
"1": 7,
"2": 7,
"3": 7,
"4": 7,
"5": 7,
"6": 7,
"7": 7,
"8": 7,
"9": 7,
":": 4,
";": 4,
"<": 7,
"=": 7,
">": 7,
"?": 7,
"@": 13,
"A": 9,
"B": 8,
"C": 8,
"D": 9,
"E": 7,
"F": 7,
"G": 9,
"H": 9,
"I": 4,
"J": 5,
"K": 8,
"L": 6,
"M": 12,
"N": 10,
"O": 10,
"P": 8,
"Q": 10,
"R": 8,
"S": 7,
"T": 7,
"U": 9,
"V": 9,
"W": 13,
"X": 8,
"Y": 7,
"Z": 7,
"[": 5,
"\\": 6,
"]": 5,
"^": 7,
"_": 7,
"`": 4,
"a": 7,
"b": 8,
"c": 6,
"d": 8,
"e": 8,
"f": 5,
"g": 7,
"h": 8,
"i": 4,
"j": 4,
"k": 7,
"l": 4,
"m": 12,
"n": 8,
"o": 8,
"p": 8,
"q": 8,
"r": 5,
"s": 6,
"t": 5,
"u": 8,
"v": 7,
"w": 11,
"x": 7,
"y": 7,
"z": 6,
"{": 5,
"|": 7,
"}": 5,
"~": 7,
}
# Compile performance critical regular expressions.
re_leading = re.compile(r"^\s")
re_trailing = re.compile(r"\s$")
re_range_parts = re.compile(r"(\$?)([A-Z]{1,3})(\$?)(\d+)")
def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False):
"""
Convert a zero indexed row and column cell reference to a A1 style string.
Args:
row: The cell row. Int.
col: The cell column. Int.
row_abs: Optional flag to make the row absolute. Bool.
col_abs: Optional flag to make the column absolute. Bool.
Returns:
A1 style string.
"""
if row < 0:
warn("Row number %d must be >= 0" % row)
return None
if col < 0:
warn("Col number %d must be >= 0" % col)
return None
row += 1 # Change to 1-index.
row_abs = "$" if row_abs else ""
col_str = xl_col_to_name(col, col_abs)
return col_str + row_abs + str(row)
def xl_rowcol_to_cell_fast(row, col):
"""
Optimized version of the xl_rowcol_to_cell function. Only used internally.
Args:
row: The cell row. Int.
col: The cell column. Int.
Returns:
A1 style string.
"""
if col in COL_NAMES:
col_str = COL_NAMES[col]
else:
col_str = xl_col_to_name(col)
COL_NAMES[col] = col_str
return col_str + str(row + 1)
def xl_col_to_name(col, col_abs=False):
"""
Convert a zero indexed column cell reference to a string.
Args:
col: The cell column. Int.
col_abs: Optional flag to make the column absolute. Bool.
Returns:
Column style string.
"""
col_num = col
if col_num < 0:
warn("Col number %d must be >= 0" % col_num)
return None
col_num += 1 # Change to 1-index.
col_str = ""
col_abs = "$" if col_abs else ""
while col_num:
# Set remainder from 1 .. 26
remainder = col_num % 26
if remainder == 0:
remainder = 26
# Convert the remainder to a character.
col_letter = chr(ord("A") + remainder - 1)
# Accumulate the column letters, right to left.
col_str = col_letter + col_str
# Get the next order of magnitude.
col_num = int((col_num - 1) / 26)
return col_abs + col_str
def xl_cell_to_rowcol(cell_str):
"""
Convert a cell reference in A1 notation to a zero indexed row and column.
Args:
cell_str: A1 style string.
Returns:
row, col: Zero indexed cell row and column indices.
"""
if not cell_str:
return 0, 0
match = re_range_parts.match(cell_str)
col_str = match.group(2)
row_str = match.group(4)
# Convert base26 column string to number.
expn = 0
col = 0
for char in reversed(col_str):
col += (ord(char) - ord("A") + 1) * (26**expn)
expn += 1
# Convert 1-index to zero-index
row = int(row_str) - 1
col -= 1
return row, col
def xl_cell_to_rowcol_abs(cell_str):
"""
Convert an absolute cell reference in A1 notation to a zero indexed
row and column, with True/False values for absolute rows or columns.
Args:
cell_str: A1 style string.
Returns:
row, col, row_abs, col_abs: Zero indexed cell row and column indices.
"""
if not cell_str:
return 0, 0, False, False
match = re_range_parts.match(cell_str)
col_abs = match.group(1)
col_str = match.group(2)
row_abs = match.group(3)
row_str = match.group(4)
if col_abs:
col_abs = True
else:
col_abs = False
if row_abs:
row_abs = True
else:
row_abs = False
# Convert base26 column string to number.
expn = 0
col = 0
for char in reversed(col_str):
col += (ord(char) - ord("A") + 1) * (26**expn)
expn += 1
# Convert 1-index to zero-index
row = int(row_str) - 1
col -= 1
return row, col, row_abs, col_abs
def xl_range(first_row, first_col, last_row, last_col):
"""
Convert zero indexed row and col cell references to a A1:B1 range string.
Args:
first_row: The first cell row. Int.
first_col: The first cell column. Int.
last_row: The last cell row. Int.
last_col: The last cell column. Int.
Returns:
A1:B1 style range string.
"""
range1 = xl_rowcol_to_cell(first_row, first_col)
range2 = xl_rowcol_to_cell(last_row, last_col)
if range1 is None or range2 is None:
warn("Row and column numbers must be >= 0")
return None
if range1 == range2:
return range1
else:
return range1 + ":" + range2
def xl_range_abs(first_row, first_col, last_row, last_col):
"""
Convert zero indexed row and col cell references to a $A$1:$B$1 absolute
range string.
Args:
first_row: The first cell row. Int.
first_col: The first cell column. Int.
last_row: The last cell row. Int.
last_col: The last cell column. Int.
Returns:
$A$1:$B$1 style range string.
"""
range1 = xl_rowcol_to_cell(first_row, first_col, True, True)
range2 = xl_rowcol_to_cell(last_row, last_col, True, True)
if range1 is None or range2 is None:
warn("Row and column numbers must be >= 0")
return None
if range1 == range2:
return range1
else:
return range1 + ":" + range2
def xl_range_formula(sheetname, first_row, first_col, last_row, last_col):
"""
Convert worksheet name and zero indexed row and col cell references to
a Sheet1!A1:B1 range formula string.
Args:
sheetname: The worksheet name. String.
first_row: The first cell row. Int.
first_col: The first cell column. Int.
last_row: The last cell row. Int.
last_col: The last cell column. Int.
Returns:
A1:B1 style range string.
"""
cell_range = xl_range_abs(first_row, first_col, last_row, last_col)
sheetname = quote_sheetname(sheetname)
return sheetname + "!" + cell_range
def quote_sheetname(sheetname):
"""
Convert a worksheet name to a quoted name if it contains spaces or
special characters.
Args:
sheetname: The worksheet name. String.
Returns:
A quoted worksheet string.
"""
if not sheetname.isalnum() and not sheetname.startswith("'"):
# Double quote any single quotes.
sheetname = sheetname.replace("'", "''")
# Single quote the sheet name.
sheetname = "'%s'" % sheetname
return sheetname
def xl_pixel_width(string):
"""
Get the pixel width of a string based on individual character widths taken
from Excel. UTF8 characters, and other unhandled characters, are given a
default width of 8.
Args:
string: The string to calculate the width for. String.
Returns:
The string width in pixels. Note, Excel adds an additional 7 pixels of
padding in the cell.
"""
length = 0
for char in string:
length += CHAR_WIDTHS.get(char, 8)
return length
def xl_color(color):
# Used in conjunction with the XlsxWriter *color() methods to convert
# a color name into an RGB formatted string. These colors are for
# backward compatibility with older versions of Excel.
named_colors = {
"black": "#000000",
"blue": "#0000FF",
"brown": "#800000",
"cyan": "#00FFFF",
"gray": "#808080",
"green": "#008000",
"lime": "#00FF00",
"magenta": "#FF00FF",
"navy": "#000080",
"orange": "#FF6600",
"pink": "#FF00FF",
"purple": "#800080",
"red": "#FF0000",
"silver": "#C0C0C0",
"white": "#FFFFFF",
"yellow": "#FFFF00",
}
if color in named_colors:
color = named_colors[color]
if not re.match("#[0-9a-fA-F]{6}", color):
warn("Color '%s' isn't a valid Excel color" % color)
# Convert the RGB color to the Excel ARGB format.
return "FF" + color.lstrip("#").upper()
def get_rgb_color(color):
# Convert the user specified color to an RGB color.
rgb_color = xl_color(color)
# Remove leading FF from RGB color for charts.
rgb_color = re.sub(r"^FF", "", rgb_color)
return rgb_color
def get_sparkline_style(style_id):
styles = [
{
"series": {"theme": "4", "tint": "-0.499984740745262"},
"negative": {"theme": "5"},
"markers": {"theme": "4", "tint": "-0.499984740745262"},
"first": {"theme": "4", "tint": "0.39997558519241921"},
"last": {"theme": "4", "tint": "0.39997558519241921"},
"high": {"theme": "4"},
"low": {"theme": "4"},
}, # 0
{
"series": {"theme": "4", "tint": "-0.499984740745262"},
"negative": {"theme": "5"},
"markers": {"theme": "4", "tint": "-0.499984740745262"},
"first": {"theme": "4", "tint": "0.39997558519241921"},
"last": {"theme": "4", "tint": "0.39997558519241921"},
"high": {"theme": "4"},
"low": {"theme": "4"},
}, # 1
{
"series": {"theme": "5", "tint": "-0.499984740745262"},
"negative": {"theme": "6"},
"markers": {"theme": "5", "tint": "-0.499984740745262"},
"first": {"theme": "5", "tint": "0.39997558519241921"},
"last": {"theme": "5", "tint": "0.39997558519241921"},
"high": {"theme": "5"},
"low": {"theme": "5"},
}, # 2
{
"series": {"theme": "6", "tint": "-0.499984740745262"},
"negative": {"theme": "7"},
"markers": {"theme": "6", "tint": "-0.499984740745262"},
"first": {"theme": "6", "tint": "0.39997558519241921"},
"last": {"theme": "6", "tint": "0.39997558519241921"},
"high": {"theme": "6"},
"low": {"theme": "6"},
}, # 3
{
"series": {"theme": "7", "tint": "-0.499984740745262"},
"negative": {"theme": "8"},
"markers": {"theme": "7", "tint": "-0.499984740745262"},
"first": {"theme": "7", "tint": "0.39997558519241921"},
"last": {"theme": "7", "tint": "0.39997558519241921"},
"high": {"theme": "7"},
"low": {"theme": "7"},
}, # 4
{
"series": {"theme": "8", "tint": "-0.499984740745262"},
"negative": {"theme": "9"},
"markers": {"theme": "8", "tint": "-0.499984740745262"},
"first": {"theme": "8", "tint": "0.39997558519241921"},
"last": {"theme": "8", "tint": "0.39997558519241921"},
"high": {"theme": "8"},
"low": {"theme": "8"},
}, # 5
{
"series": {"theme": "9", "tint": "-0.499984740745262"},
"negative": {"theme": "4"},
"markers": {"theme": "9", "tint": "-0.499984740745262"},
"first": {"theme": "9", "tint": "0.39997558519241921"},
"last": {"theme": "9", "tint": "0.39997558519241921"},
"high": {"theme": "9"},
"low": {"theme": "9"},
}, # 6
{
"series": {"theme": "4", "tint": "-0.249977111117893"},
"negative": {"theme": "5"},
"markers": {"theme": "5", "tint": "-0.249977111117893"},
"first": {"theme": "5", "tint": "-0.249977111117893"},
"last": {"theme": "5", "tint": "-0.249977111117893"},
"high": {"theme": "5", "tint": "-0.249977111117893"},
"low": {"theme": "5", "tint": "-0.249977111117893"},
}, # 7
{
"series": {"theme": "5", "tint": "-0.249977111117893"},
"negative": {"theme": "6"},
"markers": {"theme": "6", "tint": "-0.249977111117893"},
"first": {"theme": "6", "tint": "-0.249977111117893"},
"last": {"theme": "6", "tint": "-0.249977111117893"},
"high": {"theme": "6", "tint": "-0.249977111117893"},
"low": {"theme": "6", "tint": "-0.249977111117893"},
}, # 8
{
"series": {"theme": "6", "tint": "-0.249977111117893"},
"negative": {"theme": "7"},
"markers": {"theme": "7", "tint": "-0.249977111117893"},
"first": {"theme": "7", "tint": "-0.249977111117893"},
"last": {"theme": "7", "tint": "-0.249977111117893"},
"high": {"theme": "7", "tint": "-0.249977111117893"},
"low": {"theme": "7", "tint": "-0.249977111117893"},
}, # 9
{
"series": {"theme": "7", "tint": "-0.249977111117893"},
"negative": {"theme": "8"},
"markers": {"theme": "8", "tint": "-0.249977111117893"},
"first": {"theme": "8", "tint": "-0.249977111117893"},
"last": {"theme": "8", "tint": "-0.249977111117893"},
"high": {"theme": "8", "tint": "-0.249977111117893"},
"low": {"theme": "8", "tint": "-0.249977111117893"},
}, # 10
{
"series": {"theme": "8", "tint": "-0.249977111117893"},
"negative": {"theme": "9"},
"markers": {"theme": "9", "tint": "-0.249977111117893"},
"first": {"theme": "9", "tint": "-0.249977111117893"},
"last": {"theme": "9", "tint": "-0.249977111117893"},
"high": {"theme": "9", "tint": "-0.249977111117893"},
"low": {"theme": "9", "tint": "-0.249977111117893"},
}, # 11
{
"series": {"theme": "9", "tint": "-0.249977111117893"},
"negative": {"theme": "4"},
"markers": {"theme": "4", "tint": "-0.249977111117893"},
"first": {"theme": "4", "tint": "-0.249977111117893"},
"last": {"theme": "4", "tint": "-0.249977111117893"},
"high": {"theme": "4", "tint": "-0.249977111117893"},
"low": {"theme": "4", "tint": "-0.249977111117893"},
}, # 12
{
"series": {"theme": "4"},
"negative": {"theme": "5"},
"markers": {"theme": "4", "tint": "-0.249977111117893"},
"first": {"theme": "4", "tint": "-0.249977111117893"},
"last": {"theme": "4", "tint": "-0.249977111117893"},
"high": {"theme": "4", "tint": "-0.249977111117893"},
"low": {"theme": "4", "tint": "-0.249977111117893"},
}, # 13
{
"series": {"theme": "5"},
"negative": {"theme": "6"},
"markers": {"theme": "5", "tint": "-0.249977111117893"},
"first": {"theme": "5", "tint": "-0.249977111117893"},
"last": {"theme": "5", "tint": "-0.249977111117893"},
"high": {"theme": "5", "tint": "-0.249977111117893"},
"low": {"theme": "5", "tint": "-0.249977111117893"},
}, # 14
{
"series": {"theme": "6"},
"negative": {"theme": "7"},
"markers": {"theme": "6", "tint": "-0.249977111117893"},
"first": {"theme": "6", "tint": "-0.249977111117893"},
"last": {"theme": "6", "tint": "-0.249977111117893"},
"high": {"theme": "6", "tint": "-0.249977111117893"},
"low": {"theme": "6", "tint": "-0.249977111117893"},
}, # 15
{
"series": {"theme": "7"},
"negative": {"theme": "8"},
"markers": {"theme": "7", "tint": "-0.249977111117893"},
"first": {"theme": "7", "tint": "-0.249977111117893"},
"last": {"theme": "7", "tint": "-0.249977111117893"},
"high": {"theme": "7", "tint": "-0.249977111117893"},
"low": {"theme": "7", "tint": "-0.249977111117893"},
}, # 16
{
"series": {"theme": "8"},
"negative": {"theme": "9"},
"markers": {"theme": "8", "tint": "-0.249977111117893"},
"first": {"theme": "8", "tint": "-0.249977111117893"},
"last": {"theme": "8", "tint": "-0.249977111117893"},
"high": {"theme": "8", "tint": "-0.249977111117893"},
"low": {"theme": "8", "tint": "-0.249977111117893"},
}, # 17
{
"series": {"theme": "9"},
"negative": {"theme": "4"},
"markers": {"theme": "9", "tint": "-0.249977111117893"},
"first": {"theme": "9", "tint": "-0.249977111117893"},
"last": {"theme": "9", "tint": "-0.249977111117893"},
"high": {"theme": "9", "tint": "-0.249977111117893"},
"low": {"theme": "9", "tint": "-0.249977111117893"},
}, # 18
{
"series": {"theme": "4", "tint": "0.39997558519241921"},
"negative": {"theme": "0", "tint": "-0.499984740745262"},
"markers": {"theme": "4", "tint": "0.79998168889431442"},
"first": {"theme": "4", "tint": "-0.249977111117893"},
"last": {"theme": "4", "tint": "-0.249977111117893"},
"high": {"theme": "4", "tint": "-0.499984740745262"},
"low": {"theme": "4", "tint": "-0.499984740745262"},
}, # 19
{
"series": {"theme": "5", "tint": "0.39997558519241921"},
"negative": {"theme": "0", "tint": "-0.499984740745262"},
"markers": {"theme": "5", "tint": "0.79998168889431442"},
"first": {"theme": "5", "tint": "-0.249977111117893"},
"last": {"theme": "5", "tint": "-0.249977111117893"},
"high": {"theme": "5", "tint": "-0.499984740745262"},
"low": {"theme": "5", "tint": "-0.499984740745262"},
}, # 20
{
"series": {"theme": "6", "tint": "0.39997558519241921"},
"negative": {"theme": "0", "tint": "-0.499984740745262"},
"markers": {"theme": "6", "tint": "0.79998168889431442"},
"first": {"theme": "6", "tint": "-0.249977111117893"},
"last": {"theme": "6", "tint": "-0.249977111117893"},
"high": {"theme": "6", "tint": "-0.499984740745262"},
"low": {"theme": "6", "tint": "-0.499984740745262"},
}, # 21
{
"series": {"theme": "7", "tint": "0.39997558519241921"},
"negative": {"theme": "0", "tint": "-0.499984740745262"},
"markers": {"theme": "7", "tint": "0.79998168889431442"},
"first": {"theme": "7", "tint": "-0.249977111117893"},
"last": {"theme": "7", "tint": "-0.249977111117893"},
"high": {"theme": "7", "tint": "-0.499984740745262"},
"low": {"theme": "7", "tint": "-0.499984740745262"},
}, # 22
{
"series": {"theme": "8", "tint": "0.39997558519241921"},
"negative": {"theme": "0", "tint": "-0.499984740745262"},
"markers": {"theme": "8", "tint": "0.79998168889431442"},
"first": {"theme": "8", "tint": "-0.249977111117893"},
"last": {"theme": "8", "tint": "-0.249977111117893"},
"high": {"theme": "8", "tint": "-0.499984740745262"},
"low": {"theme": "8", "tint": "-0.499984740745262"},
}, # 23
{
"series": {"theme": "9", "tint": "0.39997558519241921"},
"negative": {"theme": "0", "tint": "-0.499984740745262"},
"markers": {"theme": "9", "tint": "0.79998168889431442"},
"first": {"theme": "9", "tint": "-0.249977111117893"},
"last": {"theme": "9", "tint": "-0.249977111117893"},
"high": {"theme": "9", "tint": "-0.499984740745262"},
"low": {"theme": "9", "tint": "-0.499984740745262"},
}, # 24
{
"series": {"theme": "1", "tint": "0.499984740745262"},
"negative": {"theme": "1", "tint": "0.249977111117893"},
"markers": {"theme": "1", "tint": "0.249977111117893"},
"first": {"theme": "1", "tint": "0.249977111117893"},
"last": {"theme": "1", "tint": "0.249977111117893"},
"high": {"theme": "1", "tint": "0.249977111117893"},
"low": {"theme": "1", "tint": "0.249977111117893"},
}, # 25
{
"series": {"theme": "1", "tint": "0.34998626667073579"},
"negative": {"theme": "0", "tint": "-0.249977111117893"},
"markers": {"theme": "0", "tint": "-0.249977111117893"},
"first": {"theme": "0", "tint": "-0.249977111117893"},
"last": {"theme": "0", "tint": "-0.249977111117893"},
"high": {"theme": "0", "tint": "-0.249977111117893"},
"low": {"theme": "0", "tint": "-0.249977111117893"},
}, # 26
{
"series": {"rgb": "FF323232"},
"negative": {"rgb": "FFD00000"},
"markers": {"rgb": "FFD00000"},
"first": {"rgb": "FFD00000"},
"last": {"rgb": "FFD00000"},
"high": {"rgb": "FFD00000"},
"low": {"rgb": "FFD00000"},
}, # 27
{
"series": {"rgb": "FF000000"},
"negative": {"rgb": "FF0070C0"},
"markers": {"rgb": "FF0070C0"},
"first": {"rgb": "FF0070C0"},
"last": {"rgb": "FF0070C0"},
"high": {"rgb": "FF0070C0"},
"low": {"rgb": "FF0070C0"},
}, # 28
{
"series": {"rgb": "FF376092"},
"negative": {"rgb": "FFD00000"},
"markers": {"rgb": "FFD00000"},
"first": {"rgb": "FFD00000"},
"last": {"rgb": "FFD00000"},
"high": {"rgb": "FFD00000"},
"low": {"rgb": "FFD00000"},
}, # 29
{
"series": {"rgb": "FF0070C0"},
"negative": {"rgb": "FF000000"},
"markers": {"rgb": "FF000000"},
"first": {"rgb": "FF000000"},
"last": {"rgb": "FF000000"},
"high": {"rgb": "FF000000"},
"low": {"rgb": "FF000000"},
}, # 30
{
"series": {"rgb": "FF5F5F5F"},
"negative": {"rgb": "FFFFB620"},
"markers": {"rgb": "FFD70077"},
"first": {"rgb": "FF5687C2"},
"last": {"rgb": "FF359CEB"},
"high": {"rgb": "FF56BE79"},
"low": {"rgb": "FFFF5055"},
}, # 31
{
"series": {"rgb": "FF5687C2"},
"negative": {"rgb": "FFFFB620"},
"markers": {"rgb": "FFD70077"},
"first": {"rgb": "FF777777"},
"last": {"rgb": "FF359CEB"},
"high": {"rgb": "FF56BE79"},
"low": {"rgb": "FFFF5055"},
}, # 32
{
"series": {"rgb": "FFC6EFCE"},
"negative": {"rgb": "FFFFC7CE"},
"markers": {"rgb": "FF8CADD6"},
"first": {"rgb": "FFFFDC47"},
"last": {"rgb": "FFFFEB9C"},
"high": {"rgb": "FF60D276"},
"low": {"rgb": "FFFF5367"},
}, # 33
{
"series": {"rgb": "FF00B050"},
"negative": {"rgb": "FFFF0000"},
"markers": {"rgb": "FF0070C0"},
"first": {"rgb": "FFFFC000"},
"last": {"rgb": "FFFFC000"},
"high": {"rgb": "FF00B050"},
"low": {"rgb": "FFFF0000"},
}, # 34
{
"series": {"theme": "3"},
"negative": {"theme": "9"},
"markers": {"theme": "8"},
"first": {"theme": "4"},
"last": {"theme": "5"},
"high": {"theme": "6"},
"low": {"theme": "7"},
}, # 35
{
"series": {"theme": "1"},
"negative": {"theme": "9"},
"markers": {"theme": "8"},
"first": {"theme": "4"},
"last": {"theme": "5"},
"high": {"theme": "6"},
"low": {"theme": "7"},
}, # 36
]
return styles[style_id]
def supported_datetime(dt):
# Determine is an argument is a supported datetime object.
return isinstance(
dt, (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
)
def remove_datetime_timezone(dt_obj, remove_timezone):
# Excel doesn't support timezones in datetimes/times so we remove the
# tzinfo from the object if the user has specified that option in the
# constructor.
if remove_timezone:
dt_obj = dt_obj.replace(tzinfo=None)
else:
if dt_obj.tzinfo:
raise TypeError(
"Excel doesn't support timezones in datetimes. "
"Set the tzinfo in the datetime/time object to None or "
"use the 'remove_timezone' Workbook() option"
)
return dt_obj
def datetime_to_excel_datetime(dt_obj, date_1904, remove_timezone):
# Convert a datetime object to an Excel serial date and time. The integer
# part of the number stores the number of days since the epoch and the
# fractional part stores the percentage of the day.
date_type = dt_obj
is_timedelta = False
if date_1904:
# Excel for Mac date epoch.
epoch = datetime.datetime(1904, 1, 1)
else:
# Default Excel epoch.
epoch = datetime.datetime(1899, 12, 31)
# We handle datetime .datetime, .date and .time objects but convert
# them to datetime.datetime objects and process them in the same way.
if isinstance(dt_obj, datetime.datetime):
dt_obj = remove_datetime_timezone(dt_obj, remove_timezone)
delta = dt_obj - epoch
elif isinstance(dt_obj, datetime.date):
dt_obj = datetime.datetime.fromordinal(dt_obj.toordinal())
delta = dt_obj - epoch
elif isinstance(dt_obj, datetime.time):
dt_obj = datetime.datetime.combine(epoch, dt_obj)
dt_obj = remove_datetime_timezone(dt_obj, remove_timezone)
delta = dt_obj - epoch
elif isinstance(dt_obj, datetime.timedelta):
is_timedelta = True
delta = dt_obj
else:
raise TypeError("Unknown or unsupported datetime type")
# Convert a Python datetime.datetime value to an Excel date number.
excel_time = delta.days + (
float(delta.seconds) + float(delta.microseconds) / 1e6
) / (60 * 60 * 24)
# The following is a workaround for the fact that in Excel a time only
# value is represented as 1899-12-31+time whereas in datetime.datetime()
# it is 1900-1-1+time so we need to subtract the 1 day difference.
if isinstance(date_type, datetime.datetime) and dt_obj.isocalendar() == (
1900,
1,
1,
):
excel_time -= 1
# Account for Excel erroneously treating 1900 as a leap year.
if not date_1904 and not is_timedelta and excel_time > 59:
excel_time += 1
return excel_time
def preserve_whitespace(string):
# Check if a string has leading or trailing whitespace that requires a
# "preserve" attribute.
if re_leading.search(string) or re_trailing.search(string):
return True
else:
return False

View File

@@ -0,0 +1,720 @@
###############################################################################
#
# Vml - A class for writing the Excel XLSX Vml file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
# Package imports.
from . import xmlwriter
class Vml(xmlwriter.XMLwriter):
"""
A class for writing the Excel XLSX Vml file.
"""
###########################################################################
#
# Public API.
#
###########################################################################
def __init__(self):
"""
Constructor.
"""
super(Vml, self).__init__()
###########################################################################
#
# Private API.
#
###########################################################################
def _assemble_xml_file(
self,
data_id,
vml_shape_id,
comments_data=None,
buttons_data=None,
header_images_data=None,
):
# Assemble and write the XML file.
z_index = 1
self._write_xml_namespace()
# Write the o:shapelayout element.
self._write_shapelayout(data_id)
if buttons_data:
# Write the v:shapetype element.
self._write_button_shapetype()
for button in buttons_data:
# Write the v:shape element.
vml_shape_id += 1
self._write_button_shape(vml_shape_id, z_index, button)
z_index += 1
if comments_data:
# Write the v:shapetype element.
self._write_comment_shapetype()
for comment in comments_data:
# Write the v:shape element.
vml_shape_id += 1
self._write_comment_shape(vml_shape_id, z_index, comment)
z_index += 1
if header_images_data:
# Write the v:shapetype element.
self._write_image_shapetype()
index = 1
for image in header_images_data:
# Write the v:shape element.
vml_shape_id += 1
self._write_image_shape(vml_shape_id, index, image)
index += 1
self._xml_end_tag("xml")
# Close the XML writer filehandle.
self._xml_close()
def _pixels_to_points(self, vertices):
# Convert comment vertices from pixels to points.
left, top, width, height = vertices[8:12]
# Scale to pixels.
left *= 0.75
top *= 0.75
width *= 0.75
height *= 0.75
return left, top, width, height
###########################################################################
#
# XML methods.
#
###########################################################################
def _write_xml_namespace(self):
# Write the <xml> element. This is the root element of VML.
schema = "urn:schemas-microsoft-com:"
xmlns = schema + "vml"
xmlns_o = schema + "office:office"
xmlns_x = schema + "office:excel"
attributes = [
("xmlns:v", xmlns),
("xmlns:o", xmlns_o),
("xmlns:x", xmlns_x),
]
self._xml_start_tag("xml", attributes)
def _write_shapelayout(self, data_id):
# Write the <o:shapelayout> element.
attributes = [("v:ext", "edit")]
self._xml_start_tag("o:shapelayout", attributes)
# Write the o:idmap element.
self._write_idmap(data_id)
self._xml_end_tag("o:shapelayout")
def _write_idmap(self, data_id):
# Write the <o:idmap> element.
attributes = [
("v:ext", "edit"),
("data", data_id),
]
self._xml_empty_tag("o:idmap", attributes)
def _write_comment_shapetype(self):
# Write the <v:shapetype> element.
shape_id = "_x0000_t202"
coordsize = "21600,21600"
spt = 202
path = "m,l,21600r21600,l21600,xe"
attributes = [
("id", shape_id),
("coordsize", coordsize),
("o:spt", spt),
("path", path),
]
self._xml_start_tag("v:shapetype", attributes)
# Write the v:stroke element.
self._write_stroke()
# Write the v:path element.
self._write_comment_path("t", "rect")
self._xml_end_tag("v:shapetype")
def _write_button_shapetype(self):
# Write the <v:shapetype> element.
shape_id = "_x0000_t201"
coordsize = "21600,21600"
spt = 201
path = "m,l,21600r21600,l21600,xe"
attributes = [
("id", shape_id),
("coordsize", coordsize),
("o:spt", spt),
("path", path),
]
self._xml_start_tag("v:shapetype", attributes)
# Write the v:stroke element.
self._write_stroke()
# Write the v:path element.
self._write_button_path()
# Write the o:lock element.
self._write_shapetype_lock()
self._xml_end_tag("v:shapetype")
def _write_image_shapetype(self):
# Write the <v:shapetype> element.
shape_id = "_x0000_t75"
coordsize = "21600,21600"
spt = 75
o_preferrelative = "t"
path = "m@4@5l@4@11@9@11@9@5xe"
filled = "f"
stroked = "f"
attributes = [
("id", shape_id),
("coordsize", coordsize),
("o:spt", spt),
("o:preferrelative", o_preferrelative),
("path", path),
("filled", filled),
("stroked", stroked),
]
self._xml_start_tag("v:shapetype", attributes)
# Write the v:stroke element.
self._write_stroke()
# Write the v:formulas element.
self._write_formulas()
# Write the v:path element.
self._write_image_path()
# Write the o:lock element.
self._write_aspect_ratio_lock()
self._xml_end_tag("v:shapetype")
def _write_stroke(self):
# Write the <v:stroke> element.
joinstyle = "miter"
attributes = [("joinstyle", joinstyle)]
self._xml_empty_tag("v:stroke", attributes)
def _write_comment_path(self, gradientshapeok, connecttype):
# Write the <v:path> element.
attributes = []
if gradientshapeok:
attributes.append(("gradientshapeok", "t"))
attributes.append(("o:connecttype", connecttype))
self._xml_empty_tag("v:path", attributes)
def _write_button_path(self):
# Write the <v:path> element.
shadowok = "f"
extrusionok = "f"
strokeok = "f"
fillok = "f"
connecttype = "rect"
attributes = [
("shadowok", shadowok),
("o:extrusionok", extrusionok),
("strokeok", strokeok),
("fillok", fillok),
("o:connecttype", connecttype),
]
self._xml_empty_tag("v:path", attributes)
def _write_image_path(self):
# Write the <v:path> element.
extrusionok = "f"
gradientshapeok = "t"
connecttype = "rect"
attributes = [
("o:extrusionok", extrusionok),
("gradientshapeok", gradientshapeok),
("o:connecttype", connecttype),
]
self._xml_empty_tag("v:path", attributes)
def _write_shapetype_lock(self):
# Write the <o:lock> element.
ext = "edit"
shapetype = "t"
attributes = [
("v:ext", ext),
("shapetype", shapetype),
]
self._xml_empty_tag("o:lock", attributes)
def _write_rotation_lock(self):
# Write the <o:lock> element.
ext = "edit"
rotation = "t"
attributes = [
("v:ext", ext),
("rotation", rotation),
]
self._xml_empty_tag("o:lock", attributes)
def _write_aspect_ratio_lock(self):
# Write the <o:lock> element.
ext = "edit"
aspectratio = "t"
attributes = [
("v:ext", ext),
("aspectratio", aspectratio),
]
self._xml_empty_tag("o:lock", attributes)
def _write_comment_shape(self, shape_id, z_index, comment):
# Write the <v:shape> element.
shape_type = "#_x0000_t202"
insetmode = "auto"
visibility = "hidden"
# Set the shape index.
shape_id = "_x0000_s" + str(shape_id)
# Get the comment parameters
row = comment[0]
col = comment[1]
visible = comment[4]
fillcolor = comment[5]
vertices = comment[9]
(left, top, width, height) = self._pixels_to_points(vertices)
# Set the visibility.
if visible:
visibility = "visible"
style = (
"position:absolute;"
"margin-left:%.15gpt;"
"margin-top:%.15gpt;"
"width:%.15gpt;"
"height:%.15gpt;"
"z-index:%d;"
"visibility:%s" % (left, top, width, height, z_index, visibility)
)
attributes = [
("id", shape_id),
("type", shape_type),
("style", style),
("fillcolor", fillcolor),
("o:insetmode", insetmode),
]
self._xml_start_tag("v:shape", attributes)
# Write the v:fill element.
self._write_comment_fill()
# Write the v:shadow element.
self._write_shadow()
# Write the v:path element.
self._write_comment_path(None, "none")
# Write the v:textbox element.
self._write_comment_textbox()
# Write the x:ClientData element.
self._write_comment_client_data(row, col, visible, vertices)
self._xml_end_tag("v:shape")
def _write_button_shape(self, shape_id, z_index, button):
# Write the <v:shape> element.
shape_type = "#_x0000_t201"
# Set the shape index.
shape_id = "_x0000_s" + str(shape_id)
# Get the button parameters.
# row = button["_row"]
# col = button["_col"]
vertices = button["vertices"]
(left, top, width, height) = self._pixels_to_points(vertices)
style = (
"position:absolute;"
"margin-left:%.15gpt;"
"margin-top:%.15gpt;"
"width:%.15gpt;"
"height:%.15gpt;"
"z-index:%d;"
"mso-wrap-style:tight" % (left, top, width, height, z_index)
)
attributes = [
("id", shape_id),
("type", shape_type),
]
if button.get("description"):
attributes.append(("alt", button["description"]))
attributes.append(("style", style))
attributes.append(("o:button", "t"))
attributes.append(("fillcolor", "buttonFace [67]"))
attributes.append(("strokecolor", "windowText [64]"))
attributes.append(("o:insetmode", "auto"))
self._xml_start_tag("v:shape", attributes)
# Write the v:fill element.
self._write_button_fill()
# Write the o:lock element.
self._write_rotation_lock()
# Write the v:textbox element.
self._write_button_textbox(button["font"])
# Write the x:ClientData element.
self._write_button_client_data(button)
self._xml_end_tag("v:shape")
def _write_image_shape(self, shape_id, z_index, image_data):
# Write the <v:shape> element.
shape_type = "#_x0000_t75"
# Set the shape index.
shape_id = "_x0000_s" + str(shape_id)
# Get the image parameters
width = image_data[0]
height = image_data[1]
name = image_data[2]
position = image_data[3]
x_dpi = image_data[4]
y_dpi = image_data[5]
ref_id = image_data[6]
# Scale the height/width by the resolution, relative to 72dpi.
width = width * 72.0 / x_dpi
height = height * 72.0 / y_dpi
# Excel uses a rounding based around 72 and 96 dpi.
width = 72.0 / 96 * int(width * 96.0 / 72 + 0.25)
height = 72.0 / 96 * int(height * 96.0 / 72 + 0.25)
style = (
"position:absolute;"
"margin-left:0;"
"margin-top:0;"
"width:%.15gpt;"
"height:%.15gpt;"
"z-index:%d" % (width, height, z_index)
)
attributes = [
("id", position),
("o:spid", shape_id),
("type", shape_type),
("style", style),
]
self._xml_start_tag("v:shape", attributes)
# Write the v:imagedata element.
self._write_imagedata(ref_id, name)
# Write the o:lock element.
self._write_rotation_lock()
self._xml_end_tag("v:shape")
def _write_comment_fill(self):
# Write the <v:fill> element.
color_2 = "#ffffe1"
attributes = [("color2", color_2)]
self._xml_empty_tag("v:fill", attributes)
def _write_button_fill(self):
# Write the <v:fill> element.
color_2 = "buttonFace [67]"
detectmouseclick = "t"
attributes = [
("color2", color_2),
("o:detectmouseclick", detectmouseclick),
]
self._xml_empty_tag("v:fill", attributes)
def _write_shadow(self):
# Write the <v:shadow> element.
on = "t"
color = "black"
obscured = "t"
attributes = [
("on", on),
("color", color),
("obscured", obscured),
]
self._xml_empty_tag("v:shadow", attributes)
def _write_comment_textbox(self):
# Write the <v:textbox> element.
style = "mso-direction-alt:auto"
attributes = [("style", style)]
self._xml_start_tag("v:textbox", attributes)
# Write the div element.
self._write_div("left")
self._xml_end_tag("v:textbox")
def _write_button_textbox(self, font):
# Write the <v:textbox> element.
style = "mso-direction-alt:auto"
attributes = [("style", style), ("o:singleclick", "f")]
self._xml_start_tag("v:textbox", attributes)
# Write the div element.
self._write_div("center", font)
self._xml_end_tag("v:textbox")
def _write_div(self, align, font=None):
# Write the <div> element.
style = "text-align:" + align
attributes = [("style", style)]
self._xml_start_tag("div", attributes)
if font:
# Write the font element.
self._write_font(font)
self._xml_end_tag("div")
def _write_font(self, font):
# Write the <font> element.
caption = font["caption"]
face = "Calibri"
size = 220
color = "#000000"
attributes = [
("face", face),
("size", size),
("color", color),
]
self._xml_data_element("font", caption, attributes)
def _write_comment_client_data(self, row, col, visible, vertices):
# Write the <x:ClientData> element.
object_type = "Note"
attributes = [("ObjectType", object_type)]
self._xml_start_tag("x:ClientData", attributes)
# Write the x:MoveWithCells element.
self._write_move_with_cells()
# Write the x:SizeWithCells element.
self._write_size_with_cells()
# Write the x:Anchor element.
self._write_anchor(vertices)
# Write the x:AutoFill element.
self._write_auto_fill()
# Write the x:Row element.
self._write_row(row)
# Write the x:Column element.
self._write_column(col)
# Write the x:Visible element.
if visible:
self._write_visible()
self._xml_end_tag("x:ClientData")
def _write_button_client_data(self, button):
# Write the <x:ClientData> element.
macro = button["macro"]
vertices = button["vertices"]
object_type = "Button"
attributes = [("ObjectType", object_type)]
self._xml_start_tag("x:ClientData", attributes)
# Write the x:Anchor element.
self._write_anchor(vertices)
# Write the x:PrintObject element.
self._write_print_object()
# Write the x:AutoFill element.
self._write_auto_fill()
# Write the x:FmlaMacro element.
self._write_fmla_macro(macro)
# Write the x:TextHAlign element.
self._write_text_halign()
# Write the x:TextVAlign element.
self._write_text_valign()
self._xml_end_tag("x:ClientData")
def _write_move_with_cells(self):
# Write the <x:MoveWithCells> element.
self._xml_empty_tag("x:MoveWithCells")
def _write_size_with_cells(self):
# Write the <x:SizeWithCells> element.
self._xml_empty_tag("x:SizeWithCells")
def _write_visible(self):
# Write the <x:Visible> element.
self._xml_empty_tag("x:Visible")
def _write_anchor(self, vertices):
# Write the <x:Anchor> element.
(col_start, row_start, x1, y1, col_end, row_end, x2, y2) = vertices[:8]
strings = [col_start, x1, row_start, y1, col_end, x2, row_end, y2]
strings = [str(i) for i in strings]
data = ", ".join(strings)
self._xml_data_element("x:Anchor", data)
def _write_auto_fill(self):
# Write the <x:AutoFill> element.
data = "False"
self._xml_data_element("x:AutoFill", data)
def _write_row(self, data):
# Write the <x:Row> element.
self._xml_data_element("x:Row", data)
def _write_column(self, data):
# Write the <x:Column> element.
self._xml_data_element("x:Column", data)
def _write_print_object(self):
# Write the <x:PrintObject> element.
self._xml_data_element("x:PrintObject", "False")
def _write_text_halign(self):
# Write the <x:TextHAlign> element.
self._xml_data_element("x:TextHAlign", "Center")
def _write_text_valign(self):
# Write the <x:TextVAlign> element.
self._xml_data_element("x:TextVAlign", "Center")
def _write_fmla_macro(self, data):
# Write the <x:FmlaMacro> element.
self._xml_data_element("x:FmlaMacro", data)
def _write_imagedata(self, ref_id, o_title):
# Write the <v:imagedata> element.
attributes = [
("o:relid", "rId" + str(ref_id)),
("o:title", o_title),
]
self._xml_empty_tag("v:imagedata", attributes)
def _write_formulas(self):
# Write the <v:formulas> element.
self._xml_start_tag("v:formulas")
# Write the v:f elements.
self._write_formula("if lineDrawn pixelLineWidth 0")
self._write_formula("sum @0 1 0")
self._write_formula("sum 0 0 @1")
self._write_formula("prod @2 1 2")
self._write_formula("prod @3 21600 pixelWidth")
self._write_formula("prod @3 21600 pixelHeight")
self._write_formula("sum @0 0 1")
self._write_formula("prod @6 1 2")
self._write_formula("prod @7 21600 pixelWidth")
self._write_formula("sum @8 21600 0")
self._write_formula("prod @7 21600 pixelHeight")
self._write_formula("sum @10 21600 0")
self._xml_end_tag("v:formulas")
def _write_formula(self, eqn):
# Write the <v:f> element.
attributes = [("eqn", eqn)]
self._xml_empty_tag("v:f", attributes)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,206 @@
###############################################################################
#
# XMLwriter - A base class for XlsxWriter classes.
#
# Used in conjunction with XlsxWriter.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, John McNamara, jmcnamara@cpan.org
#
# Standard packages.
import re
from io import StringIO
class XMLwriter(object):
"""
Simple XML writer class.
"""
def __init__(self):
self.fh = None
self.escapes = re.compile('["&<>\n]')
self.internal_fh = False
def _set_filehandle(self, filehandle):
# Set the writer filehandle directly. Mainly for testing.
self.fh = filehandle
self.internal_fh = False
def _set_xml_writer(self, filename):
# Set the XML writer filehandle for the object.
if isinstance(filename, StringIO):
self.internal_fh = False
self.fh = filename
else:
self.internal_fh = True
self.fh = open(filename, "w", encoding="utf-8")
def _xml_close(self):
# Close the XML filehandle if we created it.
if self.internal_fh:
self.fh.close()
def _xml_declaration(self):
# Write the XML declaration.
self.fh.write("""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n""")
def _xml_start_tag(self, tag, attributes=[]):
# Write an XML start tag with optional attributes.
for key, value in attributes:
value = self._escape_attributes(value)
tag += ' %s="%s"' % (key, value)
self.fh.write("<%s>" % tag)
def _xml_start_tag_unencoded(self, tag, attributes=[]):
# Write an XML start tag with optional, unencoded, attributes.
# This is a minor speed optimization for elements that don't
# need encoding.
for key, value in attributes:
tag += ' %s="%s"' % (key, value)
self.fh.write("<%s>" % tag)
def _xml_end_tag(self, tag):
# Write an XML end tag.
self.fh.write("</%s>" % tag)
def _xml_empty_tag(self, tag, attributes=[]):
# Write an empty XML tag with optional attributes.
for key, value in attributes:
value = self._escape_attributes(value)
tag += ' %s="%s"' % (key, value)
self.fh.write("<%s/>" % tag)
def _xml_empty_tag_unencoded(self, tag, attributes=[]):
# Write an empty XML tag with optional, unencoded, attributes.
# This is a minor speed optimization for elements that don't
# need encoding.
for key, value in attributes:
tag += ' %s="%s"' % (key, value)
self.fh.write("<%s/>" % tag)
def _xml_data_element(self, tag, data, attributes=[]):
# Write an XML element containing data with optional attributes.
end_tag = tag
for key, value in attributes:
value = self._escape_attributes(value)
tag += ' %s="%s"' % (key, value)
data = self._escape_data(data)
self.fh.write("<%s>%s</%s>" % (tag, data, end_tag))
def _xml_string_element(self, index, attributes=[]):
# Optimized tag writer for <c> cell string elements in the inner loop.
attr = ""
for key, value in attributes:
value = self._escape_attributes(value)
attr += ' %s="%s"' % (key, value)
self.fh.write("""<c%s t="s"><v>%d</v></c>""" % (attr, index))
def _xml_si_element(self, string, attributes=[]):
# Optimized tag writer for shared strings <si> elements.
attr = ""
for key, value in attributes:
value = self._escape_attributes(value)
attr += ' %s="%s"' % (key, value)
string = self._escape_data(string)
self.fh.write("""<si><t%s>%s</t></si>""" % (attr, string))
def _xml_rich_si_element(self, string):
# Optimized tag writer for shared strings <si> rich string elements.
self.fh.write("""<si>%s</si>""" % string)
def _xml_number_element(self, number, attributes=[]):
# Optimized tag writer for <c> cell number elements in the inner loop.
attr = ""
for key, value in attributes:
value = self._escape_attributes(value)
attr += ' %s="%s"' % (key, value)
self.fh.write("""<c%s><v>%.16G</v></c>""" % (attr, number))
def _xml_formula_element(self, formula, result, attributes=[]):
# Optimized tag writer for <c> cell formula elements in the inner loop.
attr = ""
for key, value in attributes:
value = self._escape_attributes(value)
attr += ' %s="%s"' % (key, value)
self.fh.write(
"""<c%s><f>%s</f><v>%s</v></c>"""
% (attr, self._escape_data(formula), self._escape_data(result))
)
def _xml_inline_string(self, string, preserve, attributes=[]):
# Optimized tag writer for inlineStr cell elements in the inner loop.
attr = ""
t_attr = ""
# Set the <t> attribute to preserve whitespace.
if preserve:
t_attr = ' xml:space="preserve"'
for key, value in attributes:
value = self._escape_attributes(value)
attr += ' %s="%s"' % (key, value)
string = self._escape_data(string)
self.fh.write(
"""<c%s t="inlineStr"><is><t%s>%s</t></is></c>""" % (attr, t_attr, string)
)
def _xml_rich_inline_string(self, string, attributes=[]):
# Optimized tag writer for rich inlineStr in the inner loop.
attr = ""
for key, value in attributes:
value = self._escape_attributes(value)
attr += ' %s="%s"' % (key, value)
self.fh.write("""<c%s t="inlineStr"><is>%s</is></c>""" % (attr, string))
def _escape_attributes(self, attribute):
# Escape XML characters in attributes.
try:
if not self.escapes.search(attribute):
return attribute
except TypeError:
return attribute
attribute = (
attribute.replace("&", "&amp;")
.replace('"', "&quot;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\n", "&#xA;")
)
return attribute
def _escape_data(self, data):
# Escape XML characters in data sections of tags. Note, this
# is different from _escape_attributes() in that double quotes
# are not escaped by Excel.
try:
if not self.escapes.search(data):
return data
except TypeError:
return data
data = data.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
return data