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