CCR/.venv/lib/python3.12/site-packages/xlsxwriter/packager.py

881 lines
29 KiB
Python

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