1#!/usr/bin/env python
2#coding:utf-8
3# Purpose: ODF Document class
4# Created: 27.12.2010
5# Copyright (C) 2010, Manfred Moitzi
6# License: MIT license
7from __future__ import unicode_literals, print_function, division
8__author__ = "mozman <mozman@gmx.at>"
9
10import os
11from .compatibility import tostr, is_bytes, is_zipfile, StringIO, is_stream
12from .const import MIMETYPES, MIMETYPE_BODYTAG_MAP, FILE_EXT_FOR_MIMETYPE
13from .xmlns import subelement, CN, etree, wrap, ALL_NSMAP, fake_element
14from .filemanager import FileManager
15from .bytestreammanager import ByteStreamManager
16from .meta import OfficeDocumentMeta
17from .styles import OfficeDocumentStyles
18from .content import OfficeDocumentContent
19from . import observer
20
21from . import body  # not used, but important to register body classes
22
23
24class InvalidFiletypeError(TypeError):
25    pass
26
27
28def is_valid_stream(buffer):
29    if is_bytes(buffer):
30        try:
31            return is_zipfile(StringIO(buffer))
32        except TypeError:
33            raise NotImplementedError("File like objects are not compatiable with zipfile in"
34                                      "Python before 2.7 version")
35    else:
36        return False
37
38
39def opendoc(filename):
40    if is_stream(filename):
41        fm = ByteStreamManager(filename)
42    else:
43        fm = FileManager(filename)
44
45    mime_type = __detect_mime_type(fm)
46    if mime_type == "application/xml":
47        try:
48            xmlnode = etree.parse(filename).getroot()
49            return FlatXMLDocument(filename=filename, xmlnode=xmlnode)
50        except etree.ParseError:
51            raise IOError("File '%s' is neither a zip-package nor a flat "
52                          "XML OpenDocumentFormat file." % filename)
53
54    return PackagedDocument(filemanager=fm, mimetype=mime_type)
55
56
57def __detect_mime_type(file_manager):
58    mime_type = file_manager.get_text('mimetype')
59    if mime_type is not None:
60        return mime_type
61    # Fall-through to next mechanism
62    entry = file_manager.manifest.find('/')
63    if entry is not None:
64        mime_type = entry.get(CN('manifest:media-type'))
65    else:
66        # use file ext name
67        ext = os.path.splitext(file_manager.zipname)[1]
68        mime_type = MIMETYPES[ext[1:]]
69    return mime_type
70
71
72def newdoc(doctype="odt", filename="", template=None):
73    if template is None:
74        mimetype = MIMETYPES[doctype]
75        document = PackagedDocument(None, mimetype)
76        document.docname = filename
77    else:
78        document = _new_doc_from_template(filename, template)
79    return document
80
81
82def _new_doc_from_template(filename, templatename):
83    # TODO: only works with zip packaged documents
84    def get_filemanager(buffer):
85        if is_stream(buffer):
86            return ByteStreamManager(buffer)
87        elif is_valid_stream(buffer):
88            return ByteStreamManager(buffer)
89        elif is_zipfile(buffer):
90            return FileManager(buffer)
91        else:
92            raise IOError('File does not exist or it is not a zipfile: %s' % tostr(buffer))
93
94    fm = get_filemanager(templatename)
95    mimetype = fm.get_text('mimetype')
96    if mimetype.endswith('-template'):
97        mimetype = mimetype[:-9]
98    try:
99        document = PackagedDocument(filemanager=fm, mimetype=mimetype)
100        document.docname = filename
101        return document
102    except KeyError:
103        raise InvalidFiletypeError("Unsupported mimetype: %s".format(mimetype))
104
105
106class _BaseDocument(object):
107    """
108    Broadcasting Events:
109        broadcast(event='prepare_saving'): send before saving the document
110        broadcast(event='post_saving'): send after saving the document
111    """
112    def __init__(self):
113        self.backup = True
114
115    def saveas(self, filename):
116        self.docname = filename
117        self.save()
118
119    def save(self):
120        if self.docname is None:
121            raise IOError('No filename specified!')
122        observer.broadcast('prepare_saving', root=self.body.get_xmlroot())
123        self.meta.touch()
124        self.meta.inc_editing_cycles()
125        self._saving_routine()
126        observer.broadcast('post_saving', root=self.body.get_xmlroot())
127
128    @property
129    def application_body_tag(self):
130        return CN(MIMETYPE_BODYTAG_MAP[self.mimetype])
131
132    def _create_shortcuts(self, body):
133        if hasattr(body, 'sheets'):
134            self.sheets = body.sheets
135        if hasattr(body, 'pages'):
136            self.pages = body.pages
137
138    def inject_style(self, stylexmlstr, where="styles.xml"):
139        style = fake_element(stylexmlstr)
140        self.styles.styles.xmlnode.append(style.xmlnode)
141
142class FlatXMLDocument(_BaseDocument):
143    """ OpenDocument contained in a single XML file. """
144    TAG = CN('office:document')
145
146    def __init__(self, filetype='odt', filename=None, xmlnode=None):
147        super(FlatXMLDocument, self).__init__()
148        self.docname=filename
149        self.mimetype = MIMETYPES[filetype]
150        self.doctype = filetype
151
152        if xmlnode is None: # new document
153            self.xmlnode = etree.Element(self.TAG, nsmap=ALL_NSMAP)
154        elif xmlnode.tag == self.TAG:
155            self.xmlnode = xmlnode
156            self.mimetype = xmlnode.get(CN('office:mimetype')) # required
157        else:
158            raise ValueError("Unexpected root tag: %s" % self.xmlnode.tag)
159
160        if self.mimetype not in frozenset(MIMETYPES.values()):
161            raise TypeError("Unsupported mimetype: %s" % self.mimetype)
162
163        self._setup()
164        self._create_shortcuts(self.body)
165
166
167    def _setup(self):
168        self.meta = OfficeDocumentMeta(subelement(self.xmlnode, CN('office:document-meta')))
169        self.styles = wrap(subelement(self.xmlnode, CN('office:settings')))
170        self.scripts = wrap(subelement(self.xmlnode, CN('office:scripts')))
171        self.fonts = wrap(subelement(self.xmlnode, CN('office:font-face-decls')))
172        self.styles = wrap(subelement(self.xmlnode, CN('office:styles')))
173        self.automatic_styles = wrap(subelement(self.xmlnode, CN('office:automatic-styles')))
174        self.master_styles = wrap(subelement(self.xmlnode, CN('office:master-styles')))
175        self.body = self.get_application_body(self.application_body_tag)
176
177    def get_application_body(self, bodytag):
178        # The office:body element is just frame element for the real document content:
179        # office:text, office:spreadsheet, office:presentation, office:drawing
180        office_body = subelement(self.xmlnode, CN('office:body'))
181        application_body = subelement(office_body, bodytag)
182        return wrap(application_body)
183
184    def _saving_routine(self):
185        if os.path.exists(self.docname) and self.backup:
186            self._backupfile(self.docname)
187        self._writefile(self.docname)
188
189    def _backupfile(self, filename):
190        bakfilename = filename+'.bak'
191        # remove existing backupfile
192        if os.path.exists(bakfilename):
193            os.remove(bakfilename)
194        os.rename(filename, bakfilename)
195
196    def _writefile(self, filename):
197        with open(filename, 'wb') as fp:
198            fp.write(self.tobytes())
199
200    def tobytes(self):
201        return etree.tostring(self.xmlnode,
202                              xml_declaration=True,
203                              encoding='UTF-8')
204
205class PackagedDocument(_BaseDocument):
206    """ OpenDocument as package in a zipfile.
207    """
208    def __init__(self, filemanager, mimetype):
209        super(PackagedDocument, self).__init__()
210        self.filemanager = fm = FileManager() if filemanager is None else filemanager
211        self.docname = fm.zipname
212
213        # add doctype to manifest
214        self.filemanager.manifest.add('/', mimetype)
215
216        self.mimetype = mimetype
217        self.doctype = FILE_EXT_FOR_MIMETYPE[mimetype]
218        fm.register('mimetype', self.mimetype)
219
220        self.meta = OfficeDocumentMeta(fm.get_xml_element('meta.xml'))
221        fm.register('meta.xml', self.meta, 'text/xml')
222
223        self.styles = OfficeDocumentStyles(fm.get_xml_element('styles.xml'))
224        fm.register('styles.xml', self.styles, 'text/xml')
225
226        self.content = OfficeDocumentContent(mimetype, fm.get_xml_element('content.xml'))
227        fm.register('content.xml', self.content, 'text/xml')
228
229        self.body = self.content.get_application_body(self.application_body_tag)
230        self._create_shortcuts(self.body)
231
232    def _saving_routine(self):
233        self.filemanager.save(self.docname, backup=self.backup)
234
235    def tobytes(self):
236        return self.filemanager.tobytes()
237