237 lines
8.2 KiB
Python
237 lines
8.2 KiB
Python
#!/usr/bin/env python
|
|
#coding:utf-8
|
|
# Purpose: ODF Document class
|
|
# Created: 27.12.2010
|
|
# Copyright (C) 2010, Manfred Moitzi
|
|
# License: MIT license
|
|
from __future__ import unicode_literals, print_function, division
|
|
__author__ = "mozman <mozman@gmx.at>"
|
|
|
|
import os
|
|
from .compatibility import tostr, is_bytes, is_zipfile, StringIO, is_stream
|
|
from .const import MIMETYPES, MIMETYPE_BODYTAG_MAP, FILE_EXT_FOR_MIMETYPE
|
|
from .xmlns import subelement, CN, etree, wrap, ALL_NSMAP, fake_element
|
|
from .filemanager import FileManager
|
|
from .bytestreammanager import ByteStreamManager
|
|
from .meta import OfficeDocumentMeta
|
|
from .styles import OfficeDocumentStyles
|
|
from .content import OfficeDocumentContent
|
|
from . import observer
|
|
|
|
from . import body # not used, but important to register body classes
|
|
|
|
|
|
class InvalidFiletypeError(TypeError):
|
|
pass
|
|
|
|
|
|
def is_valid_stream(buffer):
|
|
if is_bytes(buffer):
|
|
try:
|
|
return is_zipfile(StringIO(buffer))
|
|
except TypeError:
|
|
raise NotImplementedError("File like objects are not compatiable with zipfile in"
|
|
"Python before 2.7 version")
|
|
else:
|
|
return False
|
|
|
|
|
|
def opendoc(filename):
|
|
if is_stream(filename):
|
|
fm = ByteStreamManager(filename)
|
|
else:
|
|
fm = FileManager(filename)
|
|
|
|
mime_type = __detect_mime_type(fm)
|
|
if mime_type == "application/xml":
|
|
try:
|
|
xmlnode = etree.parse(filename).getroot()
|
|
return FlatXMLDocument(filename=filename, xmlnode=xmlnode)
|
|
except etree.ParseError:
|
|
raise IOError("File '%s' is neither a zip-package nor a flat "
|
|
"XML OpenDocumentFormat file." % filename)
|
|
|
|
return PackagedDocument(filemanager=fm, mimetype=mime_type)
|
|
|
|
|
|
def __detect_mime_type(file_manager):
|
|
mime_type = file_manager.get_text('mimetype')
|
|
if mime_type is not None:
|
|
return mime_type
|
|
# Fall-through to next mechanism
|
|
entry = file_manager.manifest.find('/')
|
|
if entry is not None:
|
|
mime_type = entry.get(CN('manifest:media-type'))
|
|
else:
|
|
# use file ext name
|
|
ext = os.path.splitext(file_manager.zipname)[1]
|
|
mime_type = MIMETYPES[ext[1:]]
|
|
return mime_type
|
|
|
|
|
|
def newdoc(doctype="odt", filename="", template=None):
|
|
if template is None:
|
|
mimetype = MIMETYPES[doctype]
|
|
document = PackagedDocument(None, mimetype)
|
|
document.docname = filename
|
|
else:
|
|
document = _new_doc_from_template(filename, template)
|
|
return document
|
|
|
|
|
|
def _new_doc_from_template(filename, templatename):
|
|
# TODO: only works with zip packaged documents
|
|
def get_filemanager(buffer):
|
|
if is_stream(buffer):
|
|
return ByteStreamManager(buffer)
|
|
elif is_valid_stream(buffer):
|
|
return ByteStreamManager(buffer)
|
|
elif is_zipfile(buffer):
|
|
return FileManager(buffer)
|
|
else:
|
|
raise IOError('File does not exist or it is not a zipfile: %s' % tostr(buffer))
|
|
|
|
fm = get_filemanager(templatename)
|
|
mimetype = fm.get_text('mimetype')
|
|
if mimetype.endswith('-template'):
|
|
mimetype = mimetype[:-9]
|
|
try:
|
|
document = PackagedDocument(filemanager=fm, mimetype=mimetype)
|
|
document.docname = filename
|
|
return document
|
|
except KeyError:
|
|
raise InvalidFiletypeError("Unsupported mimetype: %s".format(mimetype))
|
|
|
|
|
|
class _BaseDocument(object):
|
|
"""
|
|
Broadcasting Events:
|
|
broadcast(event='prepare_saving'): send before saving the document
|
|
broadcast(event='post_saving'): send after saving the document
|
|
"""
|
|
def __init__(self):
|
|
self.backup = True
|
|
|
|
def saveas(self, filename):
|
|
self.docname = filename
|
|
self.save()
|
|
|
|
def save(self):
|
|
if self.docname is None:
|
|
raise IOError('No filename specified!')
|
|
observer.broadcast('prepare_saving', root=self.body.get_xmlroot())
|
|
self.meta.touch()
|
|
self.meta.inc_editing_cycles()
|
|
self._saving_routine()
|
|
observer.broadcast('post_saving', root=self.body.get_xmlroot())
|
|
|
|
@property
|
|
def application_body_tag(self):
|
|
return CN(MIMETYPE_BODYTAG_MAP[self.mimetype])
|
|
|
|
def _create_shortcuts(self, body):
|
|
if hasattr(body, 'sheets'):
|
|
self.sheets = body.sheets
|
|
if hasattr(body, 'pages'):
|
|
self.pages = body.pages
|
|
|
|
def inject_style(self, stylexmlstr, where="styles.xml"):
|
|
style = fake_element(stylexmlstr)
|
|
self.styles.styles.xmlnode.append(style.xmlnode)
|
|
|
|
class FlatXMLDocument(_BaseDocument):
|
|
""" OpenDocument contained in a single XML file. """
|
|
TAG = CN('office:document')
|
|
|
|
def __init__(self, filetype='odt', filename=None, xmlnode=None):
|
|
super(FlatXMLDocument, self).__init__()
|
|
self.docname=filename
|
|
self.mimetype = MIMETYPES[filetype]
|
|
self.doctype = filetype
|
|
|
|
if xmlnode is None: # new document
|
|
self.xmlnode = etree.Element(self.TAG, nsmap=ALL_NSMAP)
|
|
elif xmlnode.tag == self.TAG:
|
|
self.xmlnode = xmlnode
|
|
self.mimetype = xmlnode.get(CN('office:mimetype')) # required
|
|
else:
|
|
raise ValueError("Unexpected root tag: %s" % self.xmlnode.tag)
|
|
|
|
if self.mimetype not in frozenset(MIMETYPES.values()):
|
|
raise TypeError("Unsupported mimetype: %s" % self.mimetype)
|
|
|
|
self._setup()
|
|
self._create_shortcuts(self.body)
|
|
|
|
|
|
def _setup(self):
|
|
self.meta = OfficeDocumentMeta(subelement(self.xmlnode, CN('office:document-meta')))
|
|
self.styles = wrap(subelement(self.xmlnode, CN('office:settings')))
|
|
self.scripts = wrap(subelement(self.xmlnode, CN('office:scripts')))
|
|
self.fonts = wrap(subelement(self.xmlnode, CN('office:font-face-decls')))
|
|
self.styles = wrap(subelement(self.xmlnode, CN('office:styles')))
|
|
self.automatic_styles = wrap(subelement(self.xmlnode, CN('office:automatic-styles')))
|
|
self.master_styles = wrap(subelement(self.xmlnode, CN('office:master-styles')))
|
|
self.body = self.get_application_body(self.application_body_tag)
|
|
|
|
def get_application_body(self, bodytag):
|
|
# The office:body element is just frame element for the real document content:
|
|
# office:text, office:spreadsheet, office:presentation, office:drawing
|
|
office_body = subelement(self.xmlnode, CN('office:body'))
|
|
application_body = subelement(office_body, bodytag)
|
|
return wrap(application_body)
|
|
|
|
def _saving_routine(self):
|
|
if os.path.exists(self.docname) and self.backup:
|
|
self._backupfile(self.docname)
|
|
self._writefile(self.docname)
|
|
|
|
def _backupfile(self, filename):
|
|
bakfilename = filename+'.bak'
|
|
# remove existing backupfile
|
|
if os.path.exists(bakfilename):
|
|
os.remove(bakfilename)
|
|
os.rename(filename, bakfilename)
|
|
|
|
def _writefile(self, filename):
|
|
with open(filename, 'wb') as fp:
|
|
fp.write(self.tobytes())
|
|
|
|
def tobytes(self):
|
|
return etree.tostring(self.xmlnode,
|
|
xml_declaration=True,
|
|
encoding='UTF-8')
|
|
|
|
class PackagedDocument(_BaseDocument):
|
|
""" OpenDocument as package in a zipfile.
|
|
"""
|
|
def __init__(self, filemanager, mimetype):
|
|
super(PackagedDocument, self).__init__()
|
|
self.filemanager = fm = FileManager() if filemanager is None else filemanager
|
|
self.docname = fm.zipname
|
|
|
|
# add doctype to manifest
|
|
self.filemanager.manifest.add('/', mimetype)
|
|
|
|
self.mimetype = mimetype
|
|
self.doctype = FILE_EXT_FOR_MIMETYPE[mimetype]
|
|
fm.register('mimetype', self.mimetype)
|
|
|
|
self.meta = OfficeDocumentMeta(fm.get_xml_element('meta.xml'))
|
|
fm.register('meta.xml', self.meta, 'text/xml')
|
|
|
|
self.styles = OfficeDocumentStyles(fm.get_xml_element('styles.xml'))
|
|
fm.register('styles.xml', self.styles, 'text/xml')
|
|
|
|
self.content = OfficeDocumentContent(mimetype, fm.get_xml_element('content.xml'))
|
|
fm.register('content.xml', self.content, 'text/xml')
|
|
|
|
self.body = self.content.get_application_body(self.application_body_tag)
|
|
self._create_shortcuts(self.body)
|
|
|
|
def _saving_routine(self):
|
|
self.filemanager.save(self.docname, backup=self.backup)
|
|
|
|
def tobytes(self):
|
|
return self.filemanager.tobytes()
|