1###############################################################################
2#
3# Packager - A class for writing the Excel XLSX Worksheet file.
4#
5# SPDX-License-Identifier: BSD-2-Clause
6# Copyright 2013-2024, John McNamara, jmcnamara@cpan.org
7#
8
9# Standard packages.
10import os
11import stat
12import tempfile
13from shutil import copy
14
15from io import StringIO
16from io import BytesIO
17
18# Package imports.
19from .app import App
20from .contenttypes import ContentTypes
21from .core import Core
22from .custom import Custom
23from .metadata import Metadata
24from .relationships import Relationships
25from .sharedstrings import SharedStrings
26from .rich_value import RichValue
27from .rich_value_types import RichValueTypes
28from .rich_value_rel import RichValueRel
29from .rich_value_structure import RichValueStructure
30from .styles import Styles
31from .theme import Theme
32from .vml import Vml
33from .table import Table
34from .comments import Comments
35from .exceptions import EmptyChartSeries
36
37
38class Packager(object):
39 """
40 A class for writing the Excel XLSX Packager file.
41
42 This module is used in conjunction with XlsxWriter to create an
43 Excel XLSX container file.
44
45 From Wikipedia: The Open Packaging Conventions (OPC) is a
46 container-file technology initially created by Microsoft to store
47 a combination of XML and non-XML files that together form a single
48 entity such as an Open XML Paper Specification (OpenXPS)
49 document. http://en.wikipedia.org/wiki/Open_Packaging_Conventions.
50
51 At its simplest an Excel XLSX file contains the following elements::
52
53 ____ [Content_Types].xml
54 |
55 |____ docProps
56 | |____ app.xml
57 | |____ core.xml
58 |
59 |____ xl
60 | |____ workbook.xml
61 | |____ worksheets
62 | | |____ sheet1.xml
63 | |
64 | |____ styles.xml
65 | |
66 | |____ theme
67 | | |____ theme1.xml
68 | |
69 | |_____rels
70 | |____ workbook.xml.rels
71 |
72 |_____rels
73 |____ .rels
74
75 The Packager class coordinates the classes that represent the
76 elements of the package and writes them into the XLSX file.
77
78 """
79
80 ###########################################################################
81 #
82 # Public API.
83 #
84 ###########################################################################
85
86 def __init__(self):
87 """
88 Constructor.
89
90 """
91
92 super(Packager, self).__init__()
93
94 self.tmpdir = ""
95 self.in_memory = False
96 self.workbook = None
97 self.worksheet_count = 0
98 self.chartsheet_count = 0
99 self.chart_count = 0
100 self.drawing_count = 0
101 self.table_count = 0
102 self.num_vml_files = 0
103 self.num_comment_files = 0
104 self.named_ranges = []
105 self.filenames = []
106
107 ###########################################################################
108 #
109 # Private API.
110 #
111 ###########################################################################
112
113 def _set_tmpdir(self, tmpdir):
114 # Set an optional user defined temp directory.
115 self.tmpdir = tmpdir
116
117 def _set_in_memory(self, in_memory):
118 # Set the optional 'in_memory' mode.
119 self.in_memory = in_memory
120
121 def _add_workbook(self, workbook):
122 # Add the Excel::Writer::XLSX::Workbook object to the package.
123 self.workbook = workbook
124 self.chart_count = len(workbook.charts)
125 self.drawing_count = len(workbook.drawings)
126 self.num_vml_files = workbook.num_vml_files
127 self.num_comment_files = workbook.num_comment_files
128 self.named_ranges = workbook.named_ranges
129
130 for worksheet in self.workbook.worksheets():
131 if worksheet.is_chartsheet:
132 self.chartsheet_count += 1
133 else:
134 self.worksheet_count += 1
135
136 def _create_package(self):
137 # Write the xml files that make up the XLSX OPC package.
138 self._write_content_types_file()
139 self._write_root_rels_file()
140 self._write_workbook_rels_file()
141 self._write_worksheet_files()
142 self._write_chartsheet_files()
143 self._write_workbook_file()
144 self._write_chart_files()
145 self._write_drawing_files()
146 self._write_vml_files()
147 self._write_comment_files()
148 self._write_table_files()
149 self._write_shared_strings_file()
150 self._write_styles_file()
151 self._write_custom_file()
152 self._write_theme_file()
153 self._write_worksheet_rels_files()
154 self._write_chartsheet_rels_files()
155 self._write_drawing_rels_files()
156 self._write_rich_value_rels_files()
157 self._add_image_files()
158 self._add_vba_project()
159 self._add_vba_project_signature()
160 self._write_vba_project_rels_file()
161 self._write_core_file()
162 self._write_app_file()
163 self._write_metadata_file()
164 self._write_rich_value_files()
165
166 return self.filenames
167
168 def _filename(self, xml_filename):
169 # Create a temp filename to write the XML data to and store the Excel
170 # filename to use as the name in the Zip container.
171 if self.in_memory:
172 os_filename = StringIO()
173 else:
174 (fd, os_filename) = tempfile.mkstemp(dir=self.tmpdir)
175 os.close(fd)
176
177 self.filenames.append((os_filename, xml_filename, False))
178
179 return os_filename
180
181 def _write_workbook_file(self):
182 # Write the workbook.xml file.
183 workbook = self.workbook
184
185 workbook._set_xml_writer(self._filename("xl/workbook.xml"))
186 workbook._assemble_xml_file()
187
188 def _write_worksheet_files(self):
189 # Write the worksheet files.
190 index = 1
191 for worksheet in self.workbook.worksheets():
192 if worksheet.is_chartsheet:
193 continue
194
195 if worksheet.constant_memory:
196 worksheet._opt_reopen()
197 worksheet._write_single_row()
198
199 worksheet._set_xml_writer(
200 self._filename("xl/worksheets/sheet" + str(index) + ".xml")
201 )
202 worksheet._assemble_xml_file()
203 index += 1
204
205 def _write_chartsheet_files(self):
206 # Write the chartsheet files.
207 index = 1
208 for worksheet in self.workbook.worksheets():
209 if not worksheet.is_chartsheet:
210 continue
211
212 worksheet._set_xml_writer(
213 self._filename("xl/chartsheets/sheet" + str(index) + ".xml")
214 )
215 worksheet._assemble_xml_file()
216 index += 1
217
218 def _write_chart_files(self):
219 # Write the chart files.
220 if not self.workbook.charts:
221 return
222
223 index = 1
224 for chart in self.workbook.charts:
225 # Check that the chart has at least one data series.
226 if not chart.series:
227 raise EmptyChartSeries(
228 "Chart%d must contain at least one "
229 "data series. See chart.add_series()." % index
230 )
231
232 chart._set_xml_writer(
233 self._filename("xl/charts/chart" + str(index) + ".xml")
234 )
235 chart._assemble_xml_file()
236 index += 1
237
238 def _write_drawing_files(self):
239 # Write the drawing files.
240 if not self.drawing_count:
241 return
242
243 index = 1
244 for drawing in self.workbook.drawings:
245 drawing._set_xml_writer(
246 self._filename("xl/drawings/drawing" + str(index) + ".xml")
247 )
248 drawing._assemble_xml_file()
249 index += 1
250
251 def _write_vml_files(self):
252 # Write the comment VML files.
253 index = 1
254 for worksheet in self.workbook.worksheets():
255 if not worksheet.has_vml and not worksheet.has_header_vml:
256 continue
257 if worksheet.has_vml:
258 vml = Vml()
259 vml._set_xml_writer(
260 self._filename("xl/drawings/vmlDrawing" + str(index) + ".vml")
261 )
262 vml._assemble_xml_file(
263 worksheet.vml_data_id,
264 worksheet.vml_shape_id,
265 worksheet.comments_list,
266 worksheet.buttons_list,
267 )
268 index += 1
269
270 if worksheet.has_header_vml:
271 vml = Vml()
272
273 vml._set_xml_writer(
274 self._filename("xl/drawings/vmlDrawing" + str(index) + ".vml")
275 )
276 vml._assemble_xml_file(
277 worksheet.vml_header_id,
278 worksheet.vml_header_id * 1024,
279 None,
280 None,
281 worksheet.header_images_list,
282 )
283
284 self._write_vml_drawing_rels_file(worksheet, index)
285 index += 1
286
287 def _write_comment_files(self):
288 # Write the comment files.
289 index = 1
290 for worksheet in self.workbook.worksheets():
291 if not worksheet.has_comments:
292 continue
293
294 comment = Comments()
295 comment._set_xml_writer(self._filename("xl/comments" + str(index) + ".xml"))
296 comment._assemble_xml_file(worksheet.comments_list)
297 index += 1
298
299 def _write_shared_strings_file(self):
300 # Write the sharedStrings.xml file.
301 sst = SharedStrings()
302 sst.string_table = self.workbook.str_table
303
304 if not self.workbook.str_table.count:
305 return
306
307 sst._set_xml_writer(self._filename("xl/sharedStrings.xml"))
308 sst._assemble_xml_file()
309
310 def _write_app_file(self):
311 # Write the app.xml file.
312 properties = self.workbook.doc_properties
313 app = App()
314
315 # Add the Worksheet parts.
316 worksheet_count = 0
317 for worksheet in self.workbook.worksheets():
318 if worksheet.is_chartsheet:
319 continue
320
321 # Don't write/count veryHidden sheets.
322 if worksheet.hidden != 2:
323 app._add_part_name(worksheet.name)
324 worksheet_count += 1
325
326 # Add the Worksheet heading pairs.
327 app._add_heading_pair(["Worksheets", worksheet_count])
328
329 # Add the Chartsheet parts.
330 for worksheet in self.workbook.worksheets():
331 if not worksheet.is_chartsheet:
332 continue
333 app._add_part_name(worksheet.name)
334
335 # Add the Chartsheet heading pairs.
336 app._add_heading_pair(["Charts", self.chartsheet_count])
337
338 # Add the Named Range heading pairs.
339 if self.named_ranges:
340 app._add_heading_pair(["Named Ranges", len(self.named_ranges)])
341
342 # Add the Named Ranges parts.
343 for named_range in self.named_ranges:
344 app._add_part_name(named_range)
345
346 app._set_properties(properties)
347 app.doc_security = self.workbook.read_only
348
349 app._set_xml_writer(self._filename("docProps/app.xml"))
350 app._assemble_xml_file()
351
352 def _write_core_file(self):
353 # Write the core.xml file.
354 properties = self.workbook.doc_properties
355 core = Core()
356
357 core._set_properties(properties)
358 core._set_xml_writer(self._filename("docProps/core.xml"))
359 core._assemble_xml_file()
360
361 def _write_metadata_file(self):
362 # Write the metadata.xml file.
363 if not self.workbook.has_metadata:
364 return
365
366 metadata = Metadata()
367 metadata.has_dynamic_functions = self.workbook.has_dynamic_functions
368 metadata.num_embedded_images = len(self.workbook.embedded_images.images)
369
370 metadata._set_xml_writer(self._filename("xl/metadata.xml"))
371 metadata._assemble_xml_file()
372
373 def _write_rich_value_files(self):
374
375 if not self.workbook.embedded_images.has_images():
376 return
377
378 self._write_rich_value()
379 self._write_rich_value_types()
380 self._write_rich_value_structure()
381 self._write_rich_value_rel()
382
383 def _write_rich_value(self):
384 # Write the rdrichvalue.xml file.
385 filename = self._filename("xl/richData/rdrichvalue.xml")
386 xml_file = RichValue()
387 xml_file.embedded_images = self.workbook.embedded_images.images
388 xml_file._set_xml_writer(filename)
389 xml_file._assemble_xml_file()
390
391 def _write_rich_value_types(self):
392 # Write the rdRichValueTypes.xml file.
393 filename = self._filename("xl/richData/rdRichValueTypes.xml")
394 xml_file = RichValueTypes()
395 xml_file._set_xml_writer(filename)
396 xml_file._assemble_xml_file()
397
398 def _write_rich_value_structure(self):
399 # Write the rdrichvaluestructure.xml file.
400 filename = self._filename("xl/richData/rdrichvaluestructure.xml")
401 xml_file = RichValueStructure()
402 xml_file.has_embedded_descriptions = self.workbook.has_embedded_descriptions
403 xml_file._set_xml_writer(filename)
404 xml_file._assemble_xml_file()
405
406 def _write_rich_value_rel(self):
407 # Write the richValueRel.xml file.
408 filename = self._filename("xl/richData/richValueRel.xml")
409 xml_file = RichValueRel()
410 xml_file.num_embedded_images = len(self.workbook.embedded_images.images)
411 xml_file._set_xml_writer(filename)
412 xml_file._assemble_xml_file()
413
414 def _write_custom_file(self):
415 # Write the custom.xml file.
416 properties = self.workbook.custom_properties
417 custom = Custom()
418
419 if not len(properties):
420 return
421
422 custom._set_properties(properties)
423 custom._set_xml_writer(self._filename("docProps/custom.xml"))
424 custom._assemble_xml_file()
425
426 def _write_content_types_file(self):
427 # Write the ContentTypes.xml file.
428 content = ContentTypes()
429 content._add_image_types(self.workbook.image_types)
430
431 self._get_table_count()
432
433 worksheet_index = 1
434 chartsheet_index = 1
435 for worksheet in self.workbook.worksheets():
436 if worksheet.is_chartsheet:
437 content._add_chartsheet_name("sheet" + str(chartsheet_index))
438 chartsheet_index += 1
439 else:
440 content._add_worksheet_name("sheet" + str(worksheet_index))
441 worksheet_index += 1
442
443 for i in range(1, self.chart_count + 1):
444 content._add_chart_name("chart" + str(i))
445
446 for i in range(1, self.drawing_count + 1):
447 content._add_drawing_name("drawing" + str(i))
448
449 if self.num_vml_files:
450 content._add_vml_name()
451
452 for i in range(1, self.table_count + 1):
453 content._add_table_name("table" + str(i))
454
455 for i in range(1, self.num_comment_files + 1):
456 content._add_comment_name("comments" + str(i))
457
458 # Add the sharedString rel if there is string data in the workbook.
459 if self.workbook.str_table.count:
460 content._add_shared_strings()
461
462 # Add vbaProject (and optionally vbaProjectSignature) if present.
463 if self.workbook.vba_project:
464 content._add_vba_project()
465 if self.workbook.vba_project_signature:
466 content._add_vba_project_signature()
467
468 # Add the custom properties if present.
469 if self.workbook.custom_properties:
470 content._add_custom_properties()
471
472 # Add the metadata file if present.
473 if self.workbook.has_metadata:
474 content._add_metadata()
475
476 # Add the RichValue file if present.
477 if self.workbook.embedded_images.has_images():
478 content._add_rich_value()
479
480 content._set_xml_writer(self._filename("[Content_Types].xml"))
481 content._assemble_xml_file()
482
483 def _write_styles_file(self):
484 # Write the style xml file.
485 xf_formats = self.workbook.xf_formats
486 palette = self.workbook.palette
487 font_count = self.workbook.font_count
488 num_formats = self.workbook.num_formats
489 border_count = self.workbook.border_count
490 fill_count = self.workbook.fill_count
491 custom_colors = self.workbook.custom_colors
492 dxf_formats = self.workbook.dxf_formats
493 has_comments = self.workbook.has_comments
494
495 styles = Styles()
496 styles._set_style_properties(
497 [
498 xf_formats,
499 palette,
500 font_count,
501 num_formats,
502 border_count,
503 fill_count,
504 custom_colors,
505 dxf_formats,
506 has_comments,
507 ]
508 )
509
510 styles._set_xml_writer(self._filename("xl/styles.xml"))
511 styles._assemble_xml_file()
512
513 def _write_theme_file(self):
514 # Write the theme xml file.
515 theme = Theme()
516
517 theme._set_xml_writer(self._filename("xl/theme/theme1.xml"))
518 theme._assemble_xml_file()
519
520 def _write_table_files(self):
521 # Write the table files.
522 index = 1
523 for worksheet in self.workbook.worksheets():
524 table_props = worksheet.tables
525
526 if not table_props:
527 continue
528
529 for table_props in table_props:
530 table = Table()
531 table._set_xml_writer(
532 self._filename("xl/tables/table" + str(index) + ".xml")
533 )
534 table._set_properties(table_props)
535 table._assemble_xml_file()
536 index += 1
537
538 def _get_table_count(self):
539 # Count the table files. Required for the [Content_Types] file.
540 for worksheet in self.workbook.worksheets():
541 for _ in worksheet.tables:
542 self.table_count += 1
543
544 def _write_root_rels_file(self):
545 # Write the _rels/.rels xml file.
546 rels = Relationships()
547
548 rels._add_document_relationship("/officeDocument", "xl/workbook.xml")
549
550 rels._add_package_relationship("/metadata/core-properties", "docProps/core.xml")
551
552 rels._add_document_relationship("/extended-properties", "docProps/app.xml")
553
554 if self.workbook.custom_properties:
555 rels._add_document_relationship("/custom-properties", "docProps/custom.xml")
556
557 rels._set_xml_writer(self._filename("_rels/.rels"))
558
559 rels._assemble_xml_file()
560
561 def _write_workbook_rels_file(self):
562 # Write the _rels/.rels xml file.
563 rels = Relationships()
564
565 worksheet_index = 1
566 chartsheet_index = 1
567
568 for worksheet in self.workbook.worksheets():
569 if worksheet.is_chartsheet:
570 rels._add_document_relationship(
571 "/chartsheet", "chartsheets/sheet" + str(chartsheet_index) + ".xml"
572 )
573 chartsheet_index += 1
574 else:
575 rels._add_document_relationship(
576 "/worksheet", "worksheets/sheet" + str(worksheet_index) + ".xml"
577 )
578 worksheet_index += 1
579
580 rels._add_document_relationship("/theme", "theme/theme1.xml")
581 rels._add_document_relationship("/styles", "styles.xml")
582
583 # Add the sharedString rel if there is string data in the workbook.
584 if self.workbook.str_table.count:
585 rels._add_document_relationship("/sharedStrings", "sharedStrings.xml")
586
587 # Add vbaProject if present.
588 if self.workbook.vba_project:
589 rels._add_ms_package_relationship("/vbaProject", "vbaProject.bin")
590
591 # Add the metadata file if required.
592 if self.workbook.has_metadata:
593 rels._add_document_relationship("/sheetMetadata", "metadata.xml")
594
595 # Add the RichValue files if present.
596 if self.workbook.embedded_images.has_images():
597 rels._add_rich_value_relationship()
598
599 rels._set_xml_writer(self._filename("xl/_rels/workbook.xml.rels"))
600 rels._assemble_xml_file()
601
602 def _write_worksheet_rels_files(self):
603 # Write data such as hyperlinks or drawings.
604 index = 0
605 for worksheet in self.workbook.worksheets():
606 if worksheet.is_chartsheet:
607 continue
608
609 index += 1
610
611 external_links = (
612 worksheet.external_hyper_links
613 + worksheet.external_drawing_links
614 + worksheet.external_vml_links
615 + worksheet.external_background_links
616 + worksheet.external_table_links
617 + worksheet.external_comment_links
618 )
619
620 if not external_links:
621 continue
622
623 # Create the worksheet .rels dirs.
624 rels = Relationships()
625
626 for link_data in external_links:
627 rels._add_document_relationship(*link_data)
628
629 # Create .rels file such as /xl/worksheets/_rels/sheet1.xml.rels.
630 rels._set_xml_writer(
631 self._filename("xl/worksheets/_rels/sheet" + str(index) + ".xml.rels")
632 )
633 rels._assemble_xml_file()
634
635 def _write_chartsheet_rels_files(self):
636 # Write the chartsheet .rels files for links to drawing files.
637 index = 0
638 for worksheet in self.workbook.worksheets():
639 if not worksheet.is_chartsheet:
640 continue
641
642 index += 1
643
644 external_links = (
645 worksheet.external_drawing_links + worksheet.external_vml_links
646 )
647
648 if not external_links:
649 continue
650
651 # Create the chartsheet .rels xlsx_dir.
652 rels = Relationships()
653
654 for link_data in external_links:
655 rels._add_document_relationship(*link_data)
656
657 # Create .rels file such as /xl/chartsheets/_rels/sheet1.xml.rels.
658 rels._set_xml_writer(
659 self._filename("xl/chartsheets/_rels/sheet" + str(index) + ".xml.rels")
660 )
661 rels._assemble_xml_file()
662
663 def _write_drawing_rels_files(self):
664 # Write the drawing .rels files for worksheets with charts or drawings.
665 index = 0
666 for worksheet in self.workbook.worksheets():
667 if worksheet.drawing:
668 index += 1
669
670 if not worksheet.drawing_links:
671 continue
672
673 # Create the drawing .rels xlsx_dir.
674 rels = Relationships()
675
676 for drawing_data in worksheet.drawing_links:
677 rels._add_document_relationship(*drawing_data)
678
679 # Create .rels file such as /xl/drawings/_rels/sheet1.xml.rels.
680 rels._set_xml_writer(
681 self._filename("xl/drawings/_rels/drawing" + str(index) + ".xml.rels")
682 )
683 rels._assemble_xml_file()
684
685 def _write_vml_drawing_rels_file(self, worksheet, index):
686 # Write the vmlDdrawing .rels files for worksheets with images in
687 # headers or footers.
688
689 # Create the drawing .rels dir.
690 rels = Relationships()
691
692 for drawing_data in worksheet.vml_drawing_links:
693 rels._add_document_relationship(*drawing_data)
694
695 # Create .rels file such as /xl/drawings/_rels/vmlDrawing1.vml.rels.
696 rels._set_xml_writer(
697 self._filename("xl/drawings/_rels/vmlDrawing" + str(index) + ".vml.rels")
698 )
699 rels._assemble_xml_file()
700
701 def _write_vba_project_rels_file(self):
702 # Write the vbaProject.rels xml file if signed macros exist.
703 vba_project_signature = self.workbook.vba_project_signature
704
705 if not vba_project_signature:
706 return
707
708 # Create the vbaProject .rels dir.
709 rels = Relationships()
710
711 rels._add_ms_package_relationship(
712 "/vbaProjectSignature", "vbaProjectSignature.bin"
713 )
714
715 rels._set_xml_writer(self._filename("xl/_rels/vbaProject.bin.rels"))
716 rels._assemble_xml_file()
717
718 def _write_rich_value_rels_files(self):
719 # Write the richValueRel.xml.rels for embedded images.
720 if not self.workbook.embedded_images.has_images():
721 return
722
723 # Create the worksheet .rels dirs.
724 rels = Relationships()
725
726 index = 1
727 for image_data in self.workbook.embedded_images.images:
728 file_type = image_data[1]
729 image_file = f"../media/image{index}.{file_type}"
730 rels._add_document_relationship("/image", image_file)
731 index += 1
732
733 # Create .rels file such as /xl/worksheets/_rels/sheet1.xml.rels.
734 rels._set_xml_writer(self._filename("/xl/richData/_rels/richValueRel.xml.rels"))
735
736 rels._assemble_xml_file()
737
738 def _add_image_files(self):
739 # Write the /xl/media/image?.xml files.
740 workbook = self.workbook
741 index = 1
742
743 images = workbook.embedded_images.images + workbook.images
744
745 for image in images:
746 filename = image[0]
747 ext = "." + image[1]
748 image_data = image[2]
749
750 xml_image_name = "xl/media/image" + str(index) + ext
751
752 if not self.in_memory:
753 # In file mode we just write or copy the image file.
754 os_filename = self._filename(xml_image_name)
755
756 if image_data:
757 # The data is in a byte stream. Write it to the target.
758 os_file = open(os_filename, mode="wb")
759 os_file.write(image_data.getvalue())
760 os_file.close()
761 else:
762 copy(filename, os_filename)
763
764 # Allow copies of Windows read-only images to be deleted.
765 try:
766 os.chmod(
767 os_filename, os.stat(os_filename).st_mode | stat.S_IWRITE
768 )
769 except OSError:
770 pass
771 else:
772 # For in-memory mode we read the image into a stream.
773 if image_data:
774 # The data is already in a byte stream.
775 os_filename = image_data
776 else:
777 image_file = open(filename, mode="rb")
778 image_data = image_file.read()
779 os_filename = BytesIO(image_data)
780 image_file.close()
781
782 self.filenames.append((os_filename, xml_image_name, True))
783
784 index += 1
785
786 def _add_vba_project_signature(self):
787 # Copy in a vbaProjectSignature.bin file.
788 vba_project_signature = self.workbook.vba_project_signature
789 vba_project_signature_is_stream = self.workbook.vba_project_signature_is_stream
790
791 if not vba_project_signature:
792 return
793
794 xml_vba_signature_name = "xl/vbaProjectSignature.bin"
795
796 if not self.in_memory:
797 # In file mode we just write or copy the VBA project signature file.
798 os_filename = self._filename(xml_vba_signature_name)
799
800 if vba_project_signature_is_stream:
801 # The data is in a byte stream. Write it to the target.
802 os_file = open(os_filename, mode="wb")
803 os_file.write(vba_project_signature.getvalue())
804 os_file.close()
805 else:
806 copy(vba_project_signature, os_filename)
807
808 else:
809 # For in-memory mode we read the vba into a stream.
810 if vba_project_signature_is_stream:
811 # The data is already in a byte stream.
812 os_filename = vba_project_signature
813 else:
814 vba_file = open(vba_project_signature, mode="rb")
815 vba_data = vba_file.read()
816 os_filename = BytesIO(vba_data)
817 vba_file.close()
818
819 self.filenames.append((os_filename, xml_vba_signature_name, True))
820
821 def _add_vba_project(self):
822 # Copy in a vbaProject.bin file.
823 vba_project = self.workbook.vba_project
824 vba_project_is_stream = self.workbook.vba_project_is_stream
825
826 if not vba_project:
827 return
828
829 xml_vba_name = "xl/vbaProject.bin"
830
831 if not self.in_memory:
832 # In file mode we just write or copy the VBA file.
833 os_filename = self._filename(xml_vba_name)
834
835 if vba_project_is_stream:
836 # The data is in a byte stream. Write it to the target.
837 os_file = open(os_filename, mode="wb")
838 os_file.write(vba_project.getvalue())
839 os_file.close()
840 else:
841 copy(vba_project, os_filename)
842
843 else:
844 # For in-memory mode we read the vba into a stream.
845 if vba_project_is_stream:
846 # The data is already in a byte stream.
847 os_filename = vba_project
848 else:
849 vba_file = open(vba_project, mode="rb")
850 vba_data = vba_file.read()
851 os_filename = BytesIO(vba_data)
852 vba_file.close()
853
854 self.filenames.append((os_filename, xml_vba_name, True))