opendocument.py 38 KB

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