Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/odf/opendocument.py: 11%
528 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1# -*- coding: utf-8 -*-
2# Copyright (C) 2006-2010 Søren Roug, European Environment Agency
3#
4# This library is free software; you can redistribute it and/or
5# modify it under the terms of the GNU Lesser General Public
6# License as published by the Free Software Foundation; either
7# version 2.1 of the License, or (at your option) any later version.
8#
9# This library is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public
15# License along with this library; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17#
18# Contributor(s):
19#
20# Copyright (C) 2014 Georges Khaznadar <georgesk@debian.org>
21# migration to Python3, JavaDOC comments and automatic
22# build of documentation
23#
25__doc__="""Use OpenDocument to generate your documents."""
27import zipfile, time, uuid, sys, mimetypes, copy, os.path
29# to allow Python3 to access modules in the same path
30sys.path.append(os.path.dirname(__file__))
32# using BytesIO provides a cleaner interface than StringIO
33# with both Python2 and Python3: the programmer must care to
34# convert strings or unicode to bytes, which is valid for Python 2 and 3.
35from io import StringIO, BytesIO
37from odf.namespaces import *
38import odf.manifest as manifest
39import odf.meta as meta
40from odf.office import *
41import odf.element as element
42from odf.attrconverters import make_NCName
43from xml.sax.xmlreader import InputSource
44from odf.odfmanifest import manifestlist
45import codecs
47if sys.version_info[0] == 3: 47 ↛ 50line 47 didn't jump to line 50, because the condition on line 47 was never false
48 unicode=str # unicode function does not exist
50__version__= TOOLSVERSION
52_XMLPROLOGUE = u"<?xml version='1.0' encoding='UTF-8'?>\n"
54#####
55# file permission as an integer value.
56# The following syntax would be invalid for Python3:
57# UNIXPERMS = 0100644 << 16L # -rw-r--r--
58#
59# So it has been precomputed:
60# 2175008768 is the same value as 0100644 << 16L == -rw-r--r--
61####
62UNIXPERMS = 2175008768
64IS_FILENAME = 0
65IS_IMAGE = 1
66# We need at least Python 2.2
67assert sys.version_info[0]>=2 and sys.version_info[1] >= 2
69#sys.setrecursionlimit(100)
70#The recursion limit is set conservative so mistakes like
71# s=content() s.addElement(s) won't eat up too much processor time.
73###############
74# mime-types => file extensions
75###############
76odmimetypes = {
77 u'application/vnd.oasis.opendocument.text': u'.odt',
78 u'application/vnd.oasis.opendocument.text-template': u'.ott',
79 u'application/vnd.oasis.opendocument.graphics': u'.odg',
80 u'application/vnd.oasis.opendocument.graphics-template': u'.otg',
81 u'application/vnd.oasis.opendocument.presentation': u'.odp',
82 u'application/vnd.oasis.opendocument.presentation-template': u'.otp',
83 u'application/vnd.oasis.opendocument.spreadsheet': u'.ods',
84 u'application/vnd.oasis.opendocument.spreadsheet-template': u'.ots',
85 u'application/vnd.oasis.opendocument.chart': u'.odc',
86 u'application/vnd.oasis.opendocument.chart-template': u'.otc',
87 u'application/vnd.oasis.opendocument.image': u'.odi',
88 u'application/vnd.oasis.opendocument.image-template': u'.oti',
89 u'application/vnd.oasis.opendocument.formula': u'.odf',
90 u'application/vnd.oasis.opendocument.formula-template': u'.otf',
91 u'application/vnd.oasis.opendocument.text-master': u'.odm',
92 u'application/vnd.oasis.opendocument.text-web': u'.oth',
93}
95class OpaqueObject:
96 """
97 just a record to bear a filename, a mediatype and a bytes content
98 """
99 def __init__(self, filename, mediatype, content=None):
100 """
101 the constructor
102 @param filename a unicode string
103 @param mediatype a unicode string
104 @param content a byte string or None
105 """
106 assert(type(filename)==type(u""))
107 assert(type(mediatype)==type(u""))
108 assert(type(content)==type(b"") or content == None)
110 self.mediatype = mediatype
111 self.filename = filename
112 self.content = content
114class OpenDocument:
115 """
116 A class to hold the content of an OpenDocument document
117 Use the xml method to write the XML
118 source to the screen or to a file.
119 Example of use: d = OpenDocument(mimetype); fd.write(d.xml())
120 """
121 thumbnail = None
123 def __init__(self, mimetype, add_generator=True):
124 """
125 the constructor
126 @param mimetype a unicode string
127 @param add_generator a boolean
128 """
129 assert(type(mimetype)==type(u""))
130 assert(isinstance(add_generator,True.__class__))
132 self.mimetype = mimetype
133 self.childobjects = []
134 self._extra = []
135 self.folder = u"" # Always empty for toplevel documents
136 self.topnode = Document(mimetype=self.mimetype)
137 self.topnode.ownerDocument = self
139 self.clear_caches()
141 self.Pictures = {}
142 self.meta = Meta()
143 self.topnode.addElement(self.meta)
144 if add_generator:
145 self.meta.addElement(meta.Generator(text=TOOLSVERSION))
146 self.scripts = Scripts()
147 self.topnode.addElement(self.scripts)
148 self.fontfacedecls = FontFaceDecls()
149 self.topnode.addElement(self.fontfacedecls)
150 self.settings = Settings()
151 self.topnode.addElement(self.settings)
152 self.styles = Styles()
153 self.topnode.addElement(self.styles)
154 self.automaticstyles = AutomaticStyles()
155 self.topnode.addElement(self.automaticstyles)
156 self.masterstyles = MasterStyles()
157 self.topnode.addElement(self.masterstyles)
158 self.body = Body()
159 self.topnode.addElement(self.body)
161 def rebuild_caches(self, node=None):
162 if node is None: node = self.topnode
163 self.build_caches(node)
164 for e in node.childNodes:
165 if e.nodeType == element.Node.ELEMENT_NODE:
166 self.rebuild_caches(e)
168 def clear_caches(self):
169 """
170 Clears internal caches
171 """
172 self.element_dict = {}
173 self._styles_dict = {}
174 self._styles_ooo_fix = {}
176 def build_caches(self, elt):
177 """
178 Builds internal caches; called from element.py
179 @param elt an element.Element instance
180 """
181 # assert(isinstance(elt, element.Element))
182 # why do I need this more intricated assertion?
183 # with Python3, the type of elt pops out as odf.element.Element
184 # in one test ???
185 import odf.element
186 assert(isinstance(elt, element.Element) or isinstance(elt, odf.element.Element) )
188 if elt.qname not in self.element_dict:
189 self.element_dict[elt.qname] = []
190 self.element_dict[elt.qname].append(elt)
191 if elt.qname == (STYLENS, u'style'):
192 self.__register_stylename(elt) # Add to style dictionary
193 styleref = elt.getAttrNS(TEXTNS,u'style-name')
194 if styleref is not None and styleref in self._styles_ooo_fix:
195 elt.setAttrNS(TEXTNS,u'style-name', self._styles_ooo_fix[styleref])
197 def remove_from_caches(self, elt):
198 """
199 Updates internal caches when an element has been removed
200 @param elt an element.Element instance
201 """
202 # See remark in build_caches about the following assertion
203 import odf.element
204 assert(isinstance(elt, element.Element) or isinstance(elt, odf.element.Element))
206 self.element_dict[elt.qname].remove(elt)
207 for e in elt.childNodes:
208 if e.nodeType == element.Node.ELEMENT_NODE:
209 self.remove_from_caches(e)
211 if elt.qname == (STYLENS, u'style'):
212 del self._styles_dict[elt.getAttrNS(STYLENS, u'name')]
214 def __register_stylename(self, elt):
215 '''
216 Register a style. But there are three style dictionaries:
217 office:styles, office:automatic-styles and office:master-styles
218 Chapter 14.
219 @param elt an element.Element instance
220 '''
221 assert(isinstance(elt, element.Element))
223 name = elt.getAttrNS(STYLENS, u'name')
224 if name is None:
225 return
226 if elt.parentNode.qname in ((OFFICENS,u'styles'), (OFFICENS,u'automatic-styles')):
227 if name in self._styles_dict:
228 newname = u'M'+name # Rename style
229 self._styles_ooo_fix[name] = newname
230 # From here on all references to the old name will refer to the new one
231 name = newname
232 elt.setAttrNS(STYLENS, u'name', name)
233 self._styles_dict[name] = elt
235 def toXml(self, filename=u''):
236 """
237 converts the document to a valid Xml format.
238 @param filename unicode string: the name of a file, defaults to
239 an empty string.
240 @return if filename is not empty, the XML code will be written into it
241 and the method returns None; otherwise the method returns a StringIO
242 containing valid XML.
243 Then a ".getvalue()" should return a unicode string.
244 """
245 assert(type(filename)==type(u""))
247 result=None
248 xml=StringIO()
249 if sys.version_info[0]==2:
250 xml.write(_XMLPROLOGUE)
251 else:
252 xml.write(_XMLPROLOGUE)
253 self.body.toXml(0, xml)
254 if not filename:
255 result=xml.getvalue()
256 else:
257 f=codecs.open(filename,'w', encoding='utf-8')
258 f.write(xml.getvalue())
259 f.close()
260 return result
262 def xml(self):
263 """
264 Generates the full document as an XML "file"
265 @return a bytestream in UTF-8 encoding
266 """
267 self.__replaceGenerator()
268 xml=StringIO()
269 if sys.version_info[0]==2:
270 xml.write(_XMLPROLOGUE)
271 else:
272 xml.write(_XMLPROLOGUE)
273 self.topnode.toXml(0, xml)
274 return xml.getvalue().encode("utf-8")
277 def contentxml(self):
278 """
279 Generates the content.xml file
280 @return a bytestream in UTF-8 encoding
281 """
282 xml=StringIO()
283 xml.write(_XMLPROLOGUE)
284 x = DocumentContent()
285 x.write_open_tag(0, xml)
286 if self.scripts.hasChildNodes():
287 self.scripts.toXml(1, xml)
288 if self.fontfacedecls.hasChildNodes():
289 self.fontfacedecls.toXml(1, xml)
290 a = AutomaticStyles()
291 stylelist = self._used_auto_styles([self.styles, self.automaticstyles, self.body])
292 if len(stylelist) > 0:
293 a.write_open_tag(1, xml)
294 for s in stylelist:
295 s.toXml(2, xml)
296 a.write_close_tag(1, xml)
297 else:
298 a.toXml(1, xml)
299 self.body.toXml(1, xml)
300 x.write_close_tag(0, xml)
301 return xml.getvalue().encode("utf-8")
303 def __manifestxml(self):
304 """
305 Generates the manifest.xml file;
306 The self.manifest isn't avaible unless the document is being saved
307 @return a unicode string
308 """
309 xml=StringIO()
310 xml.write(_XMLPROLOGUE)
311 self.manifest.toXml(0,xml)
312 result=xml.getvalue()
313 assert(type(result)==type(u""))
314 return result
316 def metaxml(self):
317 """
318 Generates the meta.xml file
319 @return a unicode string
320 """
321 self.__replaceGenerator()
322 x = DocumentMeta()
323 x.addElement(self.meta)
324 xml=StringIO()
325 xml.write(_XMLPROLOGUE)
326 x.toXml(0,xml)
327 result=xml.getvalue()
328 assert(type(result)==type(u""))
329 return result
331 def settingsxml(self):
332 """
333 Generates the settings.xml file
334 @return a unicode string
335 """
336 x = DocumentSettings()
337 x.addElement(self.settings)
338 xml=StringIO()
339 if sys.version_info[0]==2:
340 xml.write(_XMLPROLOGUE)
341 else:
342 xml.write(_XMLPROLOGUE)
343 x.toXml(0,xml)
344 result=xml.getvalue()
345 assert(type(result)==type(u""))
346 return result
348 def _parseoneelement(self, top, stylenamelist):
349 """
350 Finds references to style objects in master-styles
351 and add the style name to the style list if not already there.
352 Recursive
353 @return the list of style names as unicode strings
354 """
355 for e in top.childNodes:
356 if e.nodeType == element.Node.ELEMENT_NODE:
357 for styleref in (
358 (CHARTNS,u'style-name'),
359 (DRAWNS,u'style-name'),
360 (DRAWNS,u'text-style-name'),
361 (PRESENTATIONNS,u'style-name'),
362 (STYLENS,u'data-style-name'),
363 (STYLENS,u'list-style-name'),
364 (STYLENS,u'page-layout-name'),
365 (STYLENS,u'style-name'),
366 (TABLENS,u'default-cell-style-name'),
367 (TABLENS,u'style-name'),
368 (TEXTNS,u'style-name') ):
369 if e.getAttrNS(styleref[0],styleref[1]):
370 stylename = e.getAttrNS(styleref[0],styleref[1])
371 if stylename not in stylenamelist:
372 # due to the polymorphism of e.getAttrNS(),
373 # a unicode type is enforced for elements
374 stylenamelist.append(unicode(stylename))
375 stylenamelist = self._parseoneelement(e, stylenamelist)
376 return stylenamelist
378 def _used_auto_styles(self, segments):
379 """
380 Loop through the masterstyles elements, and find the automatic
381 styles that are used. These will be added to the automatic-styles
382 element in styles.xml
383 @return a list of element.Element instances
384 """
385 stylenamelist = []
386 for top in segments:
387 stylenamelist = self._parseoneelement(top, stylenamelist)
388 stylelist = []
389 for e in self.automaticstyles.childNodes:
390 if isinstance(e, element.Element) and e.getAttrNS(STYLENS,u'name') in stylenamelist:
391 stylelist.append(e)
393 # check the type of the returned data
394 ok=True
395 for e in stylelist: ok = ok and isinstance(e, element.Element)
396 assert(ok)
398 return stylelist
400 def stylesxml(self):
401 """
402 Generates the styles.xml file
403 @return valid XML code as a unicode string
404 """
405 xml=StringIO()
406 xml.write(_XMLPROLOGUE)
407 x = DocumentStyles()
408 x.write_open_tag(0, xml)
409 if self.fontfacedecls.hasChildNodes():
410 self.fontfacedecls.toXml(1, xml)
411 self.styles.toXml(1, xml)
412 a = AutomaticStyles()
413 a.write_open_tag(1, xml)
414 for s in self._used_auto_styles([self.masterstyles]):
415 s.toXml(2, xml)
416 a.write_close_tag(1, xml)
417 if self.masterstyles.hasChildNodes():
418 self.masterstyles.toXml(1, xml)
419 x.write_close_tag(0, xml)
420 result = xml.getvalue()
422 assert(type(result)==type(u""))
424 return result
426 def addPicture(self, filename, mediatype=None, content=None):
427 """
428 Add a picture
429 It uses the same convention as OOo, in that it saves the picture in
430 the zipfile in the subdirectory 'Pictures'
431 If passed a file ptr, mediatype must be set
432 @param filename unicode string: name of a file for Pictures
433 @param mediatype unicode string: name of a media, None by default
434 @param content bytes: content of media, None by default
435 @return a unicode string: the file name of the media, eventually
436 created on the fly
437 """
438 if content is None:
439 if mediatype is None:
440 mediatype, encoding = mimetypes.guess_type(filename)
441 if mediatype is None:
442 mediatype = u''
443 try: ext = filename[filename.rindex(u'.'):]
444 except: ext=u''
445 else:
446 ext = mimetypes.guess_extension(mediatype)
447 manifestfn = u"Pictures/%s%s" % (uuid.uuid4().hex.upper(), ext)
448 self.Pictures[manifestfn] = (IS_FILENAME, filename, mediatype)
449 content=b"" # this value is only use by the assert further
450 filename=u"" # this value is only use by the assert further
451 else:
452 manifestfn = filename
453 self.Pictures[manifestfn] = (IS_IMAGE, content, mediatype)
455 assert(type(filename)==type(u""))
456 assert(type(content) == type(b""))
458 return manifestfn
460 def addPictureFromFile(self, filename, mediatype=None):
461 """
462 Add a picture
463 It uses the same convention as OOo, in that it saves the picture in
464 the zipfile in the subdirectory 'Pictures'.
465 If mediatype is not given, it will be guessed from the filename
466 extension.
467 @param filesname unicode string: name of an image file
468 @param mediatype unicode string: type of media, dfaults to None
469 @return a unicode string, the name of the created file
470 """
471 if mediatype is None:
472 mediatype, encoding = mimetypes.guess_type(filename)
473 if mediatype is None:
474 mediatype = u''
475 try: ext = filename[filename.rindex(u'.'):]
476 except ValueError: ext=u''
477 else:
478 ext = mimetypes.guess_extension(mediatype)
479 manifestfn = u"Pictures/%s%s" % (uuid.uuid4().hex.upper(), ext)
480 self.Pictures[manifestfn] = (IS_FILENAME, filename, mediatype)
482 assert(type(filename)==type(u""))
483 assert(type(mediatype)==type(u""))
485 return manifestfn
487 def addPictureFromString(self, content, mediatype):
488 """
489 Add a picture from contents given as a Byte string.
490 It uses the same convention as OOo, in that it saves the picture in
491 the zipfile in the subdirectory 'Pictures'. The content variable
492 is a string that contains the binary image data. The mediatype
493 indicates the image format.
494 @param content bytes: content of media
495 @param mediatype unicode string: name of a media
496 @return a unicode string, the name of the created file
497 """
498 assert(type(content)==type(b""))
499 assert(type(mediatype)==type(u""))
501 ext = mimetypes.guess_extension(mediatype)
502 manifestfn = u"Pictures/%s%s" % (uuid.uuid4().hex.upper(), ext)
503 self.Pictures[manifestfn] = (IS_IMAGE, content, mediatype)
504 return manifestfn
506 def addThumbnail(self, filecontent=None):
507 """
508 Add a fixed thumbnail
509 The thumbnail in the library is big, so this is pretty useless.
510 @param filecontent bytes: the content of a file; defaults to None
511 """
512 assert(type(filecontent)==type(b""))
514 if filecontent is None:
515 import thumbnail
516 self.thumbnail = thumbnail.thumbnail()
517 else:
518 self.thumbnail = filecontent
520 def addObject(self, document, objectname=None):
521 """
522 Adds an object (subdocument). The object must be an OpenDocument class
523 @param document OpenDocument instance
524 @param objectname unicode string: the name of an object to add
525 @return a unicode string: the folder name in the zipfile the object is
526 stored in.
527 """
528 assert(isinstance(document, OpenDocument))
529 assert(type(objectname)==type(u"") or objectname == None)
531 self.childobjects.append(document)
532 if objectname is None:
533 document.folder = u"%s/Object %d" % (self.folder, len(self.childobjects))
534 else:
535 document.folder = objectname
536 return u".%s" % document.folder
538 def _savePictures(self, anObject, folder):
539 """
540 saves pictures contained in an object
541 @param anObject instance of OpenDocument containing pictures
542 @param folder unicode string: place to save pictures
543 """
544 assert(isinstance(anObject, OpenDocument))
545 assert(type(folder)==type(u""))
547 hasPictures = False
548 for arcname, picturerec in anObject.Pictures.items():
549 what_it_is, fileobj, mediatype = picturerec
550 self.manifest.addElement(manifest.FileEntry(fullpath=u"%s%s" % ( folder ,arcname), mediatype=mediatype))
551 hasPictures = True
552 if what_it_is == IS_FILENAME:
553 self._z.write(fileobj, folder + arcname, zipfile.ZIP_STORED)
554 else:
555 zi = zipfile.ZipInfo(str(arcname), self._now)
556 zi.compress_type = zipfile.ZIP_STORED
557 zi.external_attr = UNIXPERMS
558 self._z.writestr(zi, fileobj)
559 # According to section 17.7.3 in ODF 1.1, the pictures folder should not have a manifest entry
560# if hasPictures:
561# self.manifest.addElement(manifest.FileEntry(fullpath="%sPictures/" % folder, mediatype=""))
562 # Look in subobjects
563 subobjectnum = 1
564 for subobject in anObject.childobjects:
565 self._savePictures(subobject, u'%sObject %d/' % (folder, subobjectnum))
566 subobjectnum += 1
568 def __replaceGenerator(self):
569 """
570 Removes a previous 'generator' stance and declares TOOLSVERSION
571 as the new generator.
572 Section 3.1.1: The application MUST NOT export the original identifier
573 belonging to the application that created the document.
574 """
575 for m in self.meta.childNodes[:]:
576 if m.qname == (METANS, u'generator'):
577 self.meta.removeChild(m)
578 self.meta.addElement(meta.Generator(text=TOOLSVERSION))
580 def save(self, outputfile, addsuffix=False):
581 """
582 Save the document under the filename.
583 If the filename is '-' then save to stdout
584 @param outputfile unicode string: the special name '-' is for stdout;
585 as an alternative, it can be an io.ByteIO instance which contains
586 the ZIP content.
587 @param addsuffix boolean: whether to add a suffix or not; defaults to False
588 """
590 if outputfile == u'-':
591 outputfp = zipfile.ZipFile(sys.stdout,"w")
592 else:
593 if addsuffix:
594 outputfile = outputfile + odmimetypes.get(self.mimetype,u'.xxx')
595 outputfp = zipfile.ZipFile(outputfile, "w")
596 self.__zipwrite(outputfp)
597 outputfp.close()
599 def write(self, outputfp):
600 """
601 User API to write the ODF file to an open file descriptor
602 Writes the ZIP format
603 @param outputfp open file descriptor
604 """
605 zipoutputfp = zipfile.ZipFile(outputfp,"w")
606 self.__zipwrite(zipoutputfp)
608 def __zipwrite(self, outputfp):
609 """
610 Write the document to an open file pointer
611 This is where the real work is done
612 @param outputfp instance of zipfile.ZipFile
613 """
614 assert(isinstance(outputfp, zipfile.ZipFile))
616 self._z = outputfp
617 self._now = time.localtime()[:6]
618 self.manifest = manifest.Manifest()
620 # Write mimetype
621 zi = zipfile.ZipInfo('mimetype', self._now)
622 zi.compress_type = zipfile.ZIP_STORED
623 zi.external_attr = UNIXPERMS
624 self._z.writestr(zi, self.mimetype.encode("utf-8"))
626 self._saveXmlObjects(self,u"")
628 # Write pictures
629 self._savePictures(self,u"")
631 # Write the thumbnail
632 if self.thumbnail is not None:
633 self.manifest.addElement(manifest.FileEntry(fullpath=u"Thumbnails/", mediatype=u''))
634 self.manifest.addElement(manifest.FileEntry(fullpath=u"Thumbnails/thumbnail.png", mediatype=u''))
635 zi = zipfile.ZipInfo(u"Thumbnails/thumbnail.png", self._now)
636 zi.compress_type = zipfile.ZIP_DEFLATED
637 zi.external_attr = UNIXPERMS
638 self._z.writestr(zi, self.thumbnail)
640 # Write any extra files
641 for op in self._extra:
642 if op.filename == u"META-INF/documentsignatures.xml": continue # Don't save signatures
643 self.manifest.addElement(manifest.FileEntry(fullpath=op.filename, mediatype=op.mediatype))
644 if sys.version_info[0]==3:
645 zi = zipfile.ZipInfo(op.filename, self._now)
646 else:
647 zi = zipfile.ZipInfo(op.filename.encode('utf-8'), self._now)
648 zi.compress_type = zipfile.ZIP_DEFLATED
649 zi.external_attr = UNIXPERMS
650 if op.content is not None:
651 self._z.writestr(zi, op.content)
652 # Write manifest
653 zi = zipfile.ZipInfo(u"META-INF/manifest.xml", self._now)
654 zi.compress_type = zipfile.ZIP_DEFLATED
655 zi.external_attr = UNIXPERMS
656 self._z.writestr(zi, self.__manifestxml() )
657 del self._z
658 del self._now
659 del self.manifest
662 def _saveXmlObjects(self, anObject, folder):
663 """
664 save xml objects of an opendocument to some folder
665 @param anObject instance of OpenDocument
666 @param folder unicode string place to save xml objects
667 """
668 assert(isinstance(anObject, OpenDocument))
669 assert(type(folder)==type(u""))
671 if self == anObject:
672 self.manifest.addElement(manifest.FileEntry(fullpath=u"/", mediatype=anObject.mimetype))
673 else:
674 self.manifest.addElement(manifest.FileEntry(fullpath=folder, mediatype=anObject.mimetype))
675 # Write styles
676 self.manifest.addElement(manifest.FileEntry(fullpath=u"%sstyles.xml" % folder, mediatype=u"text/xml"))
677 zi = zipfile.ZipInfo(u"%sstyles.xml" % folder, self._now)
678 zi.compress_type = zipfile.ZIP_DEFLATED
679 zi.external_attr = UNIXPERMS
680 self._z.writestr(zi, anObject.stylesxml().encode("utf-8") )
682 # Write content
683 self.manifest.addElement(manifest.FileEntry(fullpath=u"%scontent.xml" % folder, mediatype=u"text/xml"))
684 zi = zipfile.ZipInfo(u"%scontent.xml" % folder, self._now)
685 zi.compress_type = zipfile.ZIP_DEFLATED
686 zi.external_attr = UNIXPERMS
687 self._z.writestr(zi, anObject.contentxml() )
689 # Write settings
690 if anObject.settings.hasChildNodes():
691 self.manifest.addElement(manifest.FileEntry(fullpath=u"%ssettings.xml" % folder, mediatype=u"text/xml"))
692 zi = zipfile.ZipInfo(u"%ssettings.xml" % folder, self._now)
693 zi.compress_type = zipfile.ZIP_DEFLATED
694 zi.external_attr = UNIXPERMS
695 self._z.writestr(zi, anObject.settingsxml().encode("utf-8") )
697 # Write meta
698 if self == anObject:
699 self.manifest.addElement(manifest.FileEntry(fullpath=u"meta.xml", mediatype=u"text/xml"))
700 zi = zipfile.ZipInfo(u"meta.xml", self._now)
701 zi.compress_type = zipfile.ZIP_DEFLATED
702 zi.external_attr = UNIXPERMS
703 self._z.writestr(zi, anObject.metaxml().encode("utf-8") )
705 # Write subobjects
706 subobjectnum = 1
707 for subobject in anObject.childobjects:
708 self._saveXmlObjects(subobject, u'%sObject %d/' % (folder, subobjectnum))
709 subobjectnum += 1
711# Document's DOM methods
712 def createElement(self, elt):
713 """
714 Inconvenient interface to create an element, but follows XML-DOM.
715 Does not allow attributes as argument, therefore can't check grammar.
716 @param elt element.Element instance
717 @return an element.Element instance whose grammar is not checked
718 """
719 assert(isinstance(elt, element.Element))
721 # this old code is ambiguous: is 'element' the module or is it the
722 # local variable? To disambiguate this, the local variable has been
723 # renamed to 'elt'
724 #return element(check_grammar=False)
725 return elt(check_grammar=False)
727 def createTextNode(self, data):
728 """
729 Method to create a text node
730 @param data unicode string to include in the Text element
731 @return an instance of element.Text
732 """
733 assert(type(data)==type(u""))
735 return element.Text(data)
737 def createCDATASection(self, data):
738 """
739 Method to create a CDATA section
740 @param data unicode string to include in the CDATA element
741 @return an instance of element.CDATASection
742 """
743 assert(type(data)==type(u""))
745 return element.CDATASection(cdata)
747 def getMediaType(self):
748 """
749 Returns the media type
750 @result a unicode string
751 """
752 assert (type(self.mimetype)==type(u""))
754 return self.mimetype
756 def getStyleByName(self, name):
757 """
758 Finds a style object based on the name
759 @param name unicode string the name of style to search
760 @return a syle as an element.Element instance
761 """
762 assert(type(name)==type(u""))
764 ncname = make_NCName(name)
765 if self._styles_dict == {}:
766 self.rebuild_caches()
767 result=self._styles_dict.get(ncname, None)
769 assert(isinstance(result, element.Element))
770 return result
772 def getElementsByType(self, elt):
773 """
774 Gets elements based on the type, which is function from
775 text.py, draw.py etc.
776 @param elt instance of a function which returns an element.Element
777 @return a list of istances of element.Element
778 """
779 import types
780 assert(isinstance (elt, types.FunctionType))
782 obj = elt(check_grammar=False)
783 assert (isinstance(obj, element.Element))
785 if self.element_dict == {}:
786 self.rebuild_caches()
788 # This previous code was ambiguous
789 # was "element" the module name or the local variable?
790 # the local variable is renamed to "elt" to disambiguate the code
791 #return self.element_dict.get(obj.qname, [])
793 result=self.element_dict.get(obj.qname, [])
795 ok=True
796 for e in result: ok = ok and isinstance(e, element.Element)
797 assert(ok)
799 return result
801# Convenience functions
802def OpenDocumentChart():
803 """
804 Creates a chart document
805 @return an OpenDocument instance with chart mimetype
806 """
807 doc = OpenDocument(u'application/vnd.oasis.opendocument.chart')
808 doc.chart = Chart()
809 doc.body.addElement(doc.chart)
810 return doc
812def OpenDocumentDrawing():
813 """
814 Creates a drawing document
815 @return an OpenDocument instance with drawing mimetype
816 """
817 doc = OpenDocument(u'application/vnd.oasis.opendocument.graphics')
818 doc.drawing = Drawing()
819 doc.body.addElement(doc.drawing)
820 return doc
822def OpenDocumentImage():
823 """
824 Creates an image document
825 @return an OpenDocument instance with image mimetype
826 """
827 doc = OpenDocument(u'application/vnd.oasis.opendocument.image')
828 doc.image = Image()
829 doc.body.addElement(doc.image)
830 return doc
832def OpenDocumentPresentation():
833 """
834 Creates a presentation document
835 @return an OpenDocument instance with presentation mimetype
836 """
837 doc = OpenDocument(u'application/vnd.oasis.opendocument.presentation')
838 doc.presentation = Presentation()
839 doc.body.addElement(doc.presentation)
840 return doc
842def OpenDocumentSpreadsheet():
843 """
844 Creates a spreadsheet document
845 @return an OpenDocument instance with spreadsheet mimetype
846 """
847 doc = OpenDocument(u'application/vnd.oasis.opendocument.spreadsheet')
848 doc.spreadsheet = Spreadsheet()
849 doc.body.addElement(doc.spreadsheet)
850 return doc
852def OpenDocumentText():
853 """
854 Creates a text document
855 @return an OpenDocument instance with text mimetype
856 """
857 doc = OpenDocument(u'application/vnd.oasis.opendocument.text')
858 doc.text = Text()
859 doc.body.addElement(doc.text)
860 return doc
862def OpenDocumentTextMaster():
863 """
864 Creates a text master document
865 @return an OpenDocument instance with master mimetype
866 """
867 doc = OpenDocument(u'application/vnd.oasis.opendocument.text-master')
868 doc.text = Text()
869 doc.body.addElement(doc.text)
870 return doc
872def __loadxmlparts(z, manifest, doc, objectpath):
873 """
874 Parses a document from its zipfile
875 @param z an instance of zipfile.ZipFile
876 @param manifest Manifest data structured in a dictionary
877 @param doc instance of OpenDocument to feed in
878 @param objectpath unicode string: path to an object
879 """
880 assert(isinstance(z, zipfile.ZipFile))
881 assert(type(manifest)==type(dict()))
882 assert(isinstance(doc, OpenDocument))
883 assert(type(objectpath)==type(u""))
885 from odf.load import LoadParser
886 from defusedxml.sax import make_parser
887 from xml.sax import handler
889 for xmlfile in (objectpath+u'settings.xml', objectpath+u'meta.xml', objectpath+u'content.xml', objectpath+u'styles.xml'):
890 if xmlfile not in manifest:
891 continue
892 ##########################################################
893 # this one is added to debug the bad behavior with Python2
894 # which raises exceptions of type SAXParseException
895 from xml.sax._exceptions import SAXParseException
896 ##########################################################
897 try:
898 xmlpart = z.read(xmlfile).decode("utf-8")
899 doc._parsing = xmlfile
901 parser = make_parser()
902 parser.setFeature(handler.feature_namespaces, 1)
903 parser.setFeature(handler.feature_external_ges, 0)
904 parser.setContentHandler(LoadParser(doc))
905 parser.setErrorHandler(handler.ErrorHandler())
907 inpsrc = InputSource()
908 #################
909 # There may be a SAXParseException triggered because of
910 # a missing xmlns prefix like meta, config, etc.
911 # So i add such declarations when needed (GK, 2014/10/21).
912 # Is there any option to prevent xmlns checks by SAX?
913 xmlpart=__fixXmlPart(xmlpart)
915 inpsrc.setByteStream(BytesIO(xmlpart.encode("utf-8")))
916 parser.parse(inpsrc)
917 del doc._parsing
918 except KeyError as v: pass
919 except SAXParseException:
920 print (u"====== SAX FAILED TO PARSE ==========\n", xmlpart)
922def __fixXmlPart(xmlpart):
923 """
924 fixes an xml code when it does not contain a set of requested
925 "xmlns:whatever" declarations.
926 added by G.K. on 2014/10/21
927 @param xmlpart unicode string: some XML code
928 @return fixed XML code
929 """
930 result=xmlpart
931 requestedPrefixes = (u'meta', u'config', u'dc', u'style',
932 u'svg', u'fo',u'draw', u'table',u'form')
933 for prefix in requestedPrefixes:
934 if u' xmlns:{prefix}'.format(prefix=prefix) not in xmlpart:
935 ###########################################
936 # fixed a bug triggered by math elements
937 # Notice: math elements are creectly exported to XHTML
938 # and best viewed with MathJax javascript.
939 # 2016-02-19 G.K.
940 ###########################################
941 try:
942 pos=result.index(u" xmlns:")
943 toInsert=u' xmlns:{prefix}="urn:oasis:names:tc:opendocument:xmlns:{prefix}:1.0"'.format(prefix=prefix)
944 result=result[:pos]+toInsert+result[pos:]
945 except:
946 pass
947 return result
950def __detectmimetype(zipfd, odffile):
951 """
952 detects the mime-type of an ODF file
953 @param zipfd an open zipfile.ZipFile instance
954 @param odffile this parameter is not used
955 @return a mime-type as a unicode string
956 """
957 assert(isinstance(zipfd, zipfile.ZipFile))
959 try:
960 mimetype = zipfd.read('mimetype').decode("utf-8")
961 return mimetype
962 except:
963 pass
964 # Fall-through to next mechanism
965 manifestpart = zipfd.read('META-INF/manifest.xml')
966 manifest = manifestlist(manifestpart)
967 for mentry,mvalue in manifest.items():
968 if mentry == "/":
969 assert(type(mvalue['media-type'])==type(u""))
970 return mvalue['media-type']
972 # Fall-through to last mechanism
973 return u'application/vnd.oasis.opendocument.text'
975def load(odffile):
976 """
977 Load an ODF file into memory
978 @param odffile unicode string: name of a file, or as an alternative,
979 an open readable stream
980 @return a reference to the structure (an OpenDocument instance)
981 """
982 z = zipfile.ZipFile(odffile)
983 mimetype = __detectmimetype(z, odffile)
984 doc = OpenDocument(mimetype, add_generator=False)
986 # Look in the manifest file to see if which of the four files there are
987 manifestpart = z.read('META-INF/manifest.xml')
988 manifest = manifestlist(manifestpart)
989 __loadxmlparts(z, manifest, doc, u'')
990 for mentry,mvalue in manifest.items():
991 if mentry[:9] == u"Pictures/" and len(mentry) > 9:
992 doc.addPicture(mvalue['full-path'], mvalue['media-type'], z.read(mentry))
993 elif mentry == u"Thumbnails/thumbnail.png":
994 doc.addThumbnail(z.read(mentry))
995 elif mentry in (u'settings.xml', u'meta.xml', u'content.xml', u'styles.xml'):
996 pass
997 # Load subobjects into structure
998 elif mentry[:7] == u"Object " and len(mentry) < 11 and mentry[-1] == u"/":
999 subdoc = OpenDocument(mvalue['media-type'], add_generator=False)
1000 doc.addObject(subdoc, u"/" + mentry[:-1])
1001 __loadxmlparts(z, manifest, subdoc, mentry)
1002 elif mentry[:7] == u"Object ":
1003 pass # Don't load subobjects as opaque objects
1004 else:
1005 if mvalue['full-path'][-1] == u'/':
1006 doc._extra.append(OpaqueObject(mvalue['full-path'], mvalue['media-type'], None))
1007 else:
1008 doc._extra.append(OpaqueObject(mvalue['full-path'], mvalue['media-type'], z.read(mentry)))
1009 # Add the SUN junk here to the struct somewhere
1010 # It is cached data, so it can be out-of-date
1011 z.close()
1012 b = doc.getElementsByType(Body)
1013 if mimetype[:39] == u'application/vnd.oasis.opendocument.text':
1014 doc.text = b[0].firstChild
1015 elif mimetype[:43] == u'application/vnd.oasis.opendocument.graphics':
1016 doc.graphics = b[0].firstChild
1017 elif mimetype[:47] == u'application/vnd.oasis.opendocument.presentation':
1018 doc.presentation = b[0].firstChild
1019 elif mimetype[:46] == u'application/vnd.oasis.opendocument.spreadsheet':
1020 doc.spreadsheet = b[0].firstChild
1021 elif mimetype[:40] == u'application/vnd.oasis.opendocument.chart':
1022 doc.chart = b[0].firstChild
1023 elif mimetype[:40] == u'application/vnd.oasis.opendocument.image':
1024 doc.image = b[0].firstChild
1025 elif mimetype[:42] == u'application/vnd.oasis.opendocument.formula':
1026 doc.formula = b[0].firstChild
1028 return doc
1030# vim: set expandtab sw=4 :