PCQRSCANER/venv/Lib/site-packages/xlsxwriter/packager.py
2019-12-22 21:51:47 +01:00

676 lines
23 KiB
Python

###############################################################################
#
# Packager - A class for writing the Excel XLSX Worksheet file.
#
# Copyright 2013-2019, John McNamara, jmcnamara@cpan.org
#
# Standard packages.
import os
import stat
import tempfile
from shutil import copy
from .compatibility 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 .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()
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._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_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()
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_format_count = self.workbook.num_format_count
border_count = self.workbook.border_count
fill_count = self.workbook.fill_count
custom_colors = self.workbook.custom_colors
dxf_formats = self.workbook.dxf_formats
styles = Styles()
styles._set_style_properties([
xf_formats,
palette,
font_count,
num_format_count,
border_count,
fill_count,
custom_colors,
dxf_formats])
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 table_props 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')
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_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_worksheet_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_worksheet_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))