1# Copyright (c) 2010-2024 openpyxl
2
3"""
4File manifest
5"""
6from mimetypes import MimeTypes
7import os.path
8
9from openpyxl.descriptors.serialisable import Serialisable
10from openpyxl.descriptors import String, Sequence
11from openpyxl.xml.functions import fromstring
12from openpyxl.xml.constants import (
13 ARC_CONTENT_TYPES,
14 ARC_THEME,
15 ARC_STYLE,
16 THEME_TYPE,
17 STYLES_TYPE,
18 CONTYPES_NS,
19 ACTIVEX,
20 CTRL,
21 VBA,
22)
23from openpyxl.xml.functions import tostring
24
25# initialise mime-types
26mimetypes = MimeTypes()
27mimetypes.add_type('application/xml', ".xml")
28mimetypes.add_type('application/vnd.openxmlformats-package.relationships+xml', ".rels")
29mimetypes.add_type("application/vnd.ms-office.vbaProject", ".bin")
30mimetypes.add_type("application/vnd.openxmlformats-officedocument.vmlDrawing", ".vml")
31mimetypes.add_type("image/x-emf", ".emf")
32
33
34class FileExtension(Serialisable):
35
36 tagname = "Default"
37
38 Extension = String()
39 ContentType = String()
40
41 def __init__(self, Extension, ContentType):
42 self.Extension = Extension
43 self.ContentType = ContentType
44
45
46class Override(Serialisable):
47
48 tagname = "Override"
49
50 PartName = String()
51 ContentType = String()
52
53 def __init__(self, PartName, ContentType):
54 self.PartName = PartName
55 self.ContentType = ContentType
56
57
58DEFAULT_TYPES = [
59 FileExtension("rels", "application/vnd.openxmlformats-package.relationships+xml"),
60 FileExtension("xml", "application/xml"),
61]
62
63DEFAULT_OVERRIDE = [
64 Override("/" + ARC_STYLE, STYLES_TYPE), # Styles
65 Override("/" + ARC_THEME, THEME_TYPE), # Theme
66 Override("/docProps/core.xml", "application/vnd.openxmlformats-package.core-properties+xml"),
67 Override("/docProps/app.xml", "application/vnd.openxmlformats-officedocument.extended-properties+xml")
68]
69
70
71class Manifest(Serialisable):
72
73 tagname = "Types"
74
75 Default = Sequence(expected_type=FileExtension, unique=True)
76 Override = Sequence(expected_type=Override, unique=True)
77 path = "[Content_Types].xml"
78
79 __elements__ = ("Default", "Override")
80
81 def __init__(self,
82 Default=(),
83 Override=(),
84 ):
85 if not Default:
86 Default = DEFAULT_TYPES
87 self.Default = Default
88 if not Override:
89 Override = DEFAULT_OVERRIDE
90 self.Override = Override
91
92
93 @property
94 def filenames(self):
95 return [part.PartName for part in self.Override]
96
97
98 @property
99 def extensions(self):
100 """
101 Map content types to file extensions
102 Skip parts without extensions
103 """
104 exts = {os.path.splitext(part.PartName)[-1] for part in self.Override}
105 return [(ext[1:], mimetypes.types_map[True][ext]) for ext in sorted(exts) if ext]
106
107
108 def to_tree(self):
109 """
110 Custom serialisation method to allow setting a default namespace
111 """
112 defaults = [t.Extension for t in self.Default]
113 for ext, mime in self.extensions:
114 if ext not in defaults:
115 mime = FileExtension(ext, mime)
116 self.Default.append(mime)
117 tree = super().to_tree()
118 tree.set("xmlns", CONTYPES_NS)
119 return tree
120
121
122 def __contains__(self, content_type):
123 """
124 Check whether a particular content type is contained
125 """
126 for t in self.Override:
127 if t.ContentType == content_type:
128 return True
129
130
131 def find(self, content_type):
132 """
133 Find specific content-type
134 """
135 try:
136 return next(self.findall(content_type))
137 except StopIteration:
138 return
139
140
141 def findall(self, content_type):
142 """
143 Find all elements of a specific content-type
144 """
145 for t in self.Override:
146 if t.ContentType == content_type:
147 yield t
148
149
150 def append(self, obj):
151 """
152 Add content object to the package manifest
153 # needs a contract...
154 """
155 ct = Override(PartName=obj.path, ContentType=obj.mime_type)
156 self.Override.append(ct)
157
158
159 def _write(self, archive, workbook):
160 """
161 Write manifest to the archive
162 """
163 self.append(workbook)
164 self._write_vba(workbook)
165 self._register_mimetypes(filenames=archive.namelist())
166 archive.writestr(self.path, tostring(self.to_tree()))
167
168
169 def _register_mimetypes(self, filenames):
170 """
171 Make sure that the mime type for all file extensions is registered
172 """
173 for fn in filenames:
174 ext = os.path.splitext(fn)[-1]
175 if not ext:
176 continue
177 mime = mimetypes.types_map[True][ext]
178 fe = FileExtension(ext[1:], mime)
179 self.Default.append(fe)
180
181
182 def _write_vba(self, workbook):
183 """
184 Add content types from cached workbook when keeping VBA
185 """
186 if workbook.vba_archive:
187 node = fromstring(workbook.vba_archive.read(ARC_CONTENT_TYPES))
188 mf = Manifest.from_tree(node)
189 filenames = self.filenames
190 for override in mf.Override:
191 if override.PartName not in (ACTIVEX, CTRL, VBA):
192 continue
193 if override.PartName not in filenames:
194 self.Override.append(override)