OOoUtils.py 19.4 KB
Newer Older
Kevin Deldycke's avatar
Kevin Deldycke committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
##############################################################################
#
# Copyright (c) 2003-2005 Nexedi SARL and Contributors. All Rights Reserved.
#                         Kevin DELDYCKE    <kevin@nexedi.com>
#                         Guillaume MICHON  <guillaume@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

30 31
import sys

32 33
from Acquisition import Implicit

Kevin Deldycke's avatar
Kevin Deldycke committed
34 35 36 37
from Products.PythonScripts.Utility import allow_class
from ZPublisher.HTTPRequest import FileUpload
from xml.dom import Node
from AccessControl import ClassSecurityInfo
38 39
from Globals import InitializeClass, get_request
from zipfile import ZipFile, ZIP_DEFLATED
40 41 42 43
try:
  from cStringIO import StringIO
except ImportError:
  from StringIO import StringIO
Kevin Deldycke's avatar
Kevin Deldycke committed
44
import imghdr
45
import random
Bartek Górny's avatar
Bartek Górny committed
46 47
from Products.ERP5Type import Permissions
from zLOG import LOG
48
from zLOG import PROBLEM
Kevin Deldycke's avatar
Kevin Deldycke committed
49

50
from OFS.Image import Pdata
Kevin Deldycke's avatar
Kevin Deldycke committed
51

52 53 54 55 56 57 58 59 60
try:
  from Ft.Xml import Parse
except ImportError:
  LOG('XMLSyncUtils', INFO, "Can't import Parse")
  class Parse:
    def __init__(self, *args, **kw):
      raise ImportError, "Sorry, it was not possible to import Ft library"


Kevin Deldycke's avatar
Kevin Deldycke committed
61 62
class CorruptedOOoFile(Exception): pass

63 64 65 66 67 68 69 70 71 72 73 74
OOo_mimeType_dict = {
  'sxw' : 'application/vnd.sun.xml.writer',
  'stw' : 'application/vnd.sun.xml.writer.template',
  'sxg' : 'application/vnd.sun.xml.writer.global',
  'sxc' : 'application/vnd.sun.xml.calc',
  'stc' : 'application/vnd.sun.xml.calc.template',
  'sxi' : 'application/vnd.sun.xml.impress',
  'sti' : 'application/vnd.sun.xml.impress.template',
  'sxd' : 'application/vnd.sun.xml.draw',
  'std' : 'application/vnd.sun.xml.draw.template',
  'sxm' : 'application/vnd.sun.xml.math',
}
Kevin Deldycke's avatar
Kevin Deldycke committed
75

76
class OOoBuilder(Implicit):
77 78 79
  """
  Tool that allows to reinject new files in a ZODB OOo document.
  """
80
  __allow_access_to_unprotected_subobjects__ = 1
81 82

  def __init__(self, document):
83
    if hasattr(document, 'data') :
84
      self._document = StringIO()
85 86 87 88 89 90 91 92 93 94 95

      if isinstance(document.data, Pdata):
        # Handle image included in the style
        dat = document.data
        while dat is not None:
          self._document.write(dat.data)
          dat = dat.next
      else:
        # Default behaviour
        self._document.write(document.data)
          
96 97 98
    elif hasattr(document, 'read') :
      self._document = document
    else :
99 100
      self._document = StringIO()
      self._document.write(document)
101
    self._image_count = 0    
102
    self._manifest_additions_list = []
103 104 105 106 107 108 109 110 111 112

  def replace(self, filename, stream):
    """
    Replaces the content of filename by stream in the archive.
    Creates a new file if filename was not already there.
    """
    try:
      zf = ZipFile(self._document, mode='a', compression=ZIP_DEFLATED)
    except RuntimeError:
      zf = ZipFile(self._document, mode='a')
113
    try:
114 115 116
      # remove the file first if it exists
      fi = zf.getinfo(filename)
      zf.filelist.remove( fi )
117
    except KeyError:
118 119
      # This is a new file
      pass
120 121
    zf.writestr(filename, stream)
    zf.close()
Bartek Górny's avatar
Bartek Górny committed
122

123 124 125 126 127 128 129 130 131
  def extract(self, filename):
    """
    Extracts a file from the archive
    """
    try:
      zf = ZipFile(self._document, mode='r', compression=ZIP_DEFLATED)
    except RuntimeError:
      zf = ZipFile(self._document, mode='r')
    return zf.read(filename)
Bartek Górny's avatar
Bartek Górny committed
132

133 134 135 136 137 138 139 140 141
  def getNameList(self):
    try:
      zf = ZipFile(self._document, mode='r', compression=ZIP_DEFLATED)
    except RuntimeError:
      zf = ZipFile(self._document, mode='r')
    li = zf.namelist()
    zf.close()
    return li

142 143 144
  def getMimeType(self):
    return self.extract('mimetype')

145
  def prepareContentXml(self, ooo_xml_file_id, xsl_content=None):
146 147 148 149 150
    """
      extracts content.xml text and prepare it :
        - add tal namespace
        - indent the xml
    """
151
    content_xml = self.extract(ooo_xml_file_id)
152
    output = StringIO()
153 154 155 156 157 158 159 160 161
    try:
      import libxml2
      import libxslt
      if xsl_content is None:
        raise ImportError
      stylesheet_doc = libxml2.parseDoc(xsl_content)
      stylesheet = libxslt.parseStylesheetDoc(stylesheet_doc)
      content_doc = libxml2.parseDoc(content_xml)
      result_doc = stylesheet.applyStylesheet(content_doc, None)
162 163 164 165 166 167
      #Declare zope namespaces
      root = result_doc.getRootElement()
      tal = root.newNs('http://xml.zope.org/namespaces/tal', 'tal')
      i18n = root.newNs('http://xml.zope.org/namespaces/i18n', 'i18n')
      metal = root.newNs('http://xml.zope.org/namespaces/metal', 'metal')
      root.setNsProp(tal, 'attributes', 'dummy python:request.RESPONSE.setHeader(\'Content-Type\', \'text/html;; charset=utf-8\')')
168 169 170
      buff = libxml2.createOutputBuffer(output, 'utf-8')
      result_doc.saveFormatFileTo(buff, 'utf-8', 1)
      stylesheet_doc.freeDoc(); content_doc.freeDoc(); result_doc.freeDoc()
171
      return output.getvalue()
172
    except ImportError:
173
      document = Parse(content_xml)
174
      document_element = document.documentElement
175 176 177 178 179 180 181 182 183 184
      tal = document.createAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:tal')
      tal.value = u'http://xml.zope.org/namespaces/tal'
      i18n = document.createAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:i18n')
      i18n.value = u'http://xml.zope.org/namespaces/i18n'
      metal = document.createAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:metal')
      metal.value = u'http://xml.zope.org/namespaces/metal'
      document_element.setAttributeNodeNS(tal)
      document_element.setAttributeNodeNS(i18n)
      document_element.setAttributeNodeNS(metal)
      document_element.setAttribute('tal:attributes', 'dummy python:request.RESPONSE.setHeader("Content-Type", "text/html;; charset=utf-8")')
185 186
      from xml.dom.ext import PrettyPrint
      PrettyPrint(document_element, output)
187
      return output.getvalue()
188

189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
  def addFileEntry(self, full_path, media_type, content=None):
      """ Add a file entry to the manifest and possibly is content """
      self.addManifest(full_path, media_type)
      if content:
          self.replace(full_path, content)

  def addManifest(self, full_path, media_type):
    """ Add a path to the manifest """
    li = '<manifest:file-entry manifest:media-type="%s" manifest:full-path="%s"/>'%(media_type, full_path)
    self._manifest_additions_list.append(li)

  def updateManifest(self):
    """ Add a path to the manifest """
    MANIFEST_FILENAME = 'META-INF/manifest.xml'
    meta_infos = self.extract(MANIFEST_FILENAME)
    # prevent some duplicates
    for meta_line in meta_infos.split('\n'):
        for new_meta_line in self._manifest_additions_list:
            if meta_line.strip() == new_meta_line:
                self._manifest_additions_list.remove(new_meta_line)

    # add the new lines
    self._manifest_additions_list.append('</manifest:manifest>')
    meta_infos = meta_infos.replace( self._manifest_additions_list[-1], '\n'.join(self._manifest_additions_list) )
    self.replace(MANIFEST_FILENAME, meta_infos)
    self._manifest_additions_list = []

216 217 218 219 220 221 222 223
  def addImage(self, image, format='png'):
    """
    Add an image to the current document and return its id
    """
    count = self._image_count
    self._image_count += 1
    name = "Picture/%s.%s" % (count, format)
    self.replace(name, image)
224
    is_legacy = ('oasis.opendocument' not in self.getMimeType())
Yoshinori Okuji's avatar
Yoshinori Okuji committed
225
    return "%s%s" % (is_legacy and '#' or '', name,)
226

227 228 229 230 231 232
  def render(self, name='', extension='sxw'):
    """
    returns the OOo document
    """
    request = get_request()
    if name:
233 234
      request.response.setHeader('Content-Disposition', 'inline; filename=%s.%s' % (name, extension))

235 236
    self._document.seek(0)
    return self._document.read()
Bartek Górny's avatar
Bartek Górny committed
237

238
allow_class(OOoBuilder)
Kevin Deldycke's avatar
Kevin Deldycke committed
239

240
class OOoParser(Implicit):
Kevin Deldycke's avatar
Kevin Deldycke committed
241 242 243
  """
    General purpose tools to parse and handle OpenOffice v1.x documents.
  """
244
  __allow_access_to_unprotected_subobjects__ = 1 
Kevin Deldycke's avatar
Kevin Deldycke committed
245 246 247 248 249 250
  def __init__(self):
    self.oo_content_dom = None
    self.oo_styles_dom  = None
    self.oo_files = {}
    self.pictures = {}
    self.ns = {}
Kevin Deldycke's avatar
Kevin Deldycke committed
251
    self.filename = None
Kevin Deldycke's avatar
Kevin Deldycke committed
252

253 254 255
  def openFromString(self, text_content):
    return self.openFile(StringIO(text_content))

256
  def openFile(self, file_descriptor):
Kevin Deldycke's avatar
Kevin Deldycke committed
257 258 259 260 261
    """
      Load all files in the zipped OpenOffice document
    """
    # Try to unzip the Open Office doc
    try:
262
      oo_unzipped = ZipFile(file_descriptor, mode="r")
Kevin Deldycke's avatar
Kevin Deldycke committed
263
    except:
264
      LOG('ERP5OOo', PROBLEM, 'Error in openFile', error=sys.exc_info())
265
      raise CorruptedOOoFile()
Kevin Deldycke's avatar
Kevin Deldycke committed
266 267
    # Test the integrity of the file
    if oo_unzipped.testzip() != None:
268
      raise CorruptedOOoFile()
Kevin Deldycke's avatar
Kevin Deldycke committed
269

Kevin Deldycke's avatar
Kevin Deldycke committed
270
    # Get the filename
271
    self.filename = getattr(file_descriptor, 'filename', 'default_filename')
Kevin Deldycke's avatar
Kevin Deldycke committed
272

Kevin Deldycke's avatar
Kevin Deldycke committed
273 274 275
    # List and load the content of the zip file
    for name in oo_unzipped.namelist():
      self.oo_files[name] = oo_unzipped.read(name)
276
    oo_unzipped.close()
Kevin Deldycke's avatar
Kevin Deldycke committed
277 278

    # Get the main content and style definitions
279 280
    self.oo_content_dom = Parse(self.oo_files["content.xml"])
    self.oo_styles_dom  = Parse(self.oo_files["styles.xml"])
Kevin Deldycke's avatar
Kevin Deldycke committed
281 282

    # Create a namespace table
283 284 285
    xpath = './/*[name() = "office:document-styles"]'
    doc_ns = self.oo_styles_dom.xpath(xpath)
    for i in range(doc_ns[0].attributes.length)[1:]:
Kevin Deldycke's avatar
Kevin Deldycke committed
286 287 288 289 290
        if doc_ns[0].attributes.item(i).nodeType == Node.ATTRIBUTE_NODE:
            name = doc_ns[0].attributes.item(i).name
            if name[:5] == "xmlns":
                self.ns[name[6:]] = doc_ns[0].attributes.item(i).value

Kevin Deldycke's avatar
Kevin Deldycke committed
291 292 293 294 295 296
  def getFilename(self):
    """
      Return the name of the OpenOffice file
    """
    return self.filename

297
  def getPicturesMapping(self):
Kevin Deldycke's avatar
Kevin Deldycke committed
298 299 300 301 302 303 304 305 306 307 308
    """
      Return a dictionnary of all pictures in the document
    """
    if len(self.pictures) <= 0:
      for file_name in self.oo_files:
        raw_data = self.oo_files[file_name]
        pict_type = imghdr.what(None, raw_data)
        if pict_type != None:
          self.pictures[file_name] = raw_data
    return self.pictures

309
  def getContentDom(self):
Kevin Deldycke's avatar
Kevin Deldycke committed
310 311 312 313 314
    """
      Return the DOM tree of the main OpenOffice content
    """
    return self.oo_content_dom

315
  def getSpreadsheetsDom(self, include_embedded=False):
316 317 318 319
    """
      Return a list of DOM tree spreadsheets (optionnaly included embedded ones)
    """
    spreadsheets = []
320
    spreadsheets = self.getPlainSpreadsheetsDom()
321
    if include_embedded == True:
322
      spreadsheets += self.getEmbeddedSpreadsheetsDom()
323 324
    return spreadsheets

325
  def getSpreadsheetsMapping(self, include_embedded=False, no_empty_lines=False, normalize=True):
326 327 328
    """
      Return a list of table-like spreadsheets (optionnaly included embedded ones)
    """
329
    tables = {}
330
    tables = self.getPlainSpreadsheetsMapping(no_empty_lines, normalize)
331
    if include_embedded == True:
332
      embedded_tables = self.getEmbeddedSpreadsheetsMapping(no_empty_lines, normalize)
333 334
      tables = self._getTableListUnion(tables, embedded_tables)
    return tables
335

336
  def getPlainSpreadsheetsDom(self):
337 338 339 340 341
    """
      Retrieve every spreadsheets from the document and get they DOM tree
    """
    spreadsheets = []
    # List all spreadsheets
342
    for table in self.oo_content_dom.xpath('.//*[name() = "table:table"]'):
343 344 345
      spreadsheets.append(table)
    return spreadsheets

346
  def getPlainSpreadsheetsMapping(self, no_empty_lines=False, normalize=True):
347 348 349
    """
      Return a list of plain spreadsheets from the document and transform them as table
    """
350
    tables = {}
351
    for spreadsheet in self.getPlainSpreadsheetsDom():
352
      new_table = self.getSpreadsheetMapping(spreadsheet, no_empty_lines, normalize)
353
      if new_table != None:
354
        tables = self._getTableListUnion(tables, new_table)
355 356
    return tables

357
  def getEmbeddedSpreadsheetsDom(self):
Kevin Deldycke's avatar
Kevin Deldycke committed
358 359 360 361 362
    """
      Return a list of existing embedded spreadsheets in the file as DOM tree
    """
    spreadsheets = []
    # List all embedded spreadsheets
363
    emb_objects = self.oo_content_dom.xpath('.//*[name() = "draw:object"]')
Kevin Deldycke's avatar
Kevin Deldycke committed
364
    for embedded in emb_objects:
365 366 367
        document = embedded.getAttributeNS(self.ns["xlink"], "href")
        if document:
            try:
368 369 370 371
                
                object_content = Parse(self.oo_files[document[3:] + '/content.xml'])
                xpath = './/*[name() = "table:table"]'
                tables = self.oo_content_dom.xpath(xpath)
372 373 374 375 376 377 378
                if tables:
                    for table in tables:
                        spreadsheets.append(table)
                else: # XXX: insert the link to OLE document ?
                    pass
            except:
                pass
Kevin Deldycke's avatar
Kevin Deldycke committed
379 380
    return spreadsheets

381
  def getEmbeddedSpreadsheetsMapping(self, no_empty_lines=False, normalize=True):
Kevin Deldycke's avatar
Kevin Deldycke committed
382
    """
383
      Return a list of embedded spreadsheets in the document as table
Kevin Deldycke's avatar
Kevin Deldycke committed
384
    """
385
    tables = {}
386
    for spreadsheet in self.getEmbeddedSpreadsheetsDom():
387
      new_table = self.getSpreadsheetMapping(spreadsheet, no_empty_lines, normalize)
Kevin Deldycke's avatar
Kevin Deldycke committed
388
      if new_table != None:
389
        tables = self._getTableListUnion(tables, new_table)
Kevin Deldycke's avatar
Kevin Deldycke committed
390 391
    return tables

392
  def getSpreadsheetMapping(self, spreadsheet=None, no_empty_lines=False, normalize=True):
Kevin Deldycke's avatar
Kevin Deldycke committed
393 394
    """
      This method convert an OpenOffice spreadsheet to a simple table.
395
      This code is based on the oo2pt tool (http://cvs.sourceforge.net/viewcvs.py/collective/CMFReportTool/oo2pt).
Kevin Deldycke's avatar
Kevin Deldycke committed
396
    """
397
    if spreadsheet == None or spreadsheet.nodeName != 'table:table':
Kevin Deldycke's avatar
Kevin Deldycke committed
398 399
      return None

400
    table = []
Kevin Deldycke's avatar
Kevin Deldycke committed
401

402 403 404
    # Get the table name
    table_name = spreadsheet.getAttributeNS(self.ns["table"], "name")

405
    # Scan table and store usable informations
406
    for line in spreadsheet.xpath('.//*[name() = "table:table-row"]'):
407 408 409 410 411 412

      # TODO : to the same as cell about abusive repeated lines

      line_group_found = line.getAttributeNS(self.ns["table"], "number-rows-repeated")
      if not line_group_found:
        lines_to_repeat = 1
413
      else:
414
        lines_to_repeat = int(line_group_found)
415

416
      for i in range(lines_to_repeat):
417 418
        table_line = []

419
        # Get all cells
420
        cells = line.xpath('.//*[name() = "table:table-cell"]')
421 422 423 424 425 426 427 428 429 430 431 432 433
        cell_index_range = range(len(cells))

        for cell_index in cell_index_range:
          cell = cells[cell_index]

          # If the cell as no child, cells have no content
          # And if the cell is the last of the row, we don't need to add it to the line
          # So we can go to the next line (= exit this cells loop)
          #
          # I must do this test because sometimes the following cell group
          #   can be found in OOo documents : <table:table-cell table:number-columns-repeated='246'/>
          # This is bad because it create too much irrevelent content that slow down the process
          # So it's a good idea to break the loop in this case
434
          if len(cell.childNodes) == 0 and cell_index == cell_index_range[-1]:
435 436 437 438 439 440
            break

          # Handle cells group
          cell_group_found = cell.getAttributeNS(self.ns["table"], "number-columns-repeated")
          if not cell_group_found:
            cells_to_repeat = 1
441
          else:
442
            cells_to_repeat = int(cell_group_found)
443

444 445 446
          # Ungroup repeated cells
          for j in range(cells_to_repeat):
            # Get the cell content
447
            cell_text = None
448
            text_tags = cell.xpath('.//*[name() = "text:p"]')
449
            for text in text_tags:
450
              for k in range(len(text.childNodes)):
451 452 453 454 455 456
                child = text.childNodes[k]
                if child.nodeType == Node.TEXT_NODE:
                  if cell_text == None:
                    cell_text = ''
                  cell_text += child.nodeValue

457
            # Add the cell to the line
458
            table_line.append(cell_text)
459

Kevin Deldycke's avatar
Kevin Deldycke committed
460 461 462 463 464 465 466 467 468
        # Delete empty lines if needed
        if no_empty_lines:
          empty_cell = 0
          for table_cell in table_line:
            if table_cell == None:
              empty_cell += 1
          if empty_cell == len(table_line):
            table_line = None

469
        # Add the line to the table
Kevin Deldycke's avatar
Kevin Deldycke committed
470 471
        if table_line != None:
          table.append(table_line)
472 473 474 475
        else:
          # If the line is empty here, the repeated line will also be empty, so
          # no need to loop.
          break
476

477
    # Reduce the table to the minimum
478 479 480 481 482 483 484 485 486 487
    new_table = self._getReducedTable(table)

    # Get a homogenized table
    if normalize:
      table_size = self._getTableSizeDict(new_table)
      new_table = self._getNormalizedBoundsTable( table  = new_table
                                                , width  = table_size['width']
                                                , height = table_size['height']
                                                )
    return {table_name: new_table}
Kevin Deldycke's avatar
Kevin Deldycke committed
488

489
  def _getReducedTable(self, table):
Kevin Deldycke's avatar
Kevin Deldycke committed
490
    """
491
      Reduce the table to its minimum size
Kevin Deldycke's avatar
Kevin Deldycke committed
492 493 494 495 496
    """
    empty_lines = 0
    no_more_empty_lines = 0

    # Eliminate all empty cells at the ends of lines and columns
497
    # Browse the table starting from the bottom for easy empty lines count
498
    for line in range(len(table)-1, -1, -1):
Kevin Deldycke's avatar
Kevin Deldycke committed
499
      empty_cells = 0
500
      line_content = table[line]
Kevin Deldycke's avatar
Kevin Deldycke committed
501
      for cell in range(len(line_content)-1, -1, -1):
502
        if line_content[cell] in ('', None):
Kevin Deldycke's avatar
Kevin Deldycke committed
503 504 505
          empty_cells += 1
        else:
          break
506

Kevin Deldycke's avatar
Kevin Deldycke committed
507 508 509 510
      if (not no_more_empty_lines) and (empty_cells == len(line_content)):
        empty_lines += 1
      else:
        line_size = len(line_content) - empty_cells
511
        table[line] = line_content[:line_size]
Kevin Deldycke's avatar
Kevin Deldycke committed
512 513
        no_more_empty_lines = 1

514 515 516
    table_height = len(table) - empty_lines

    return table[:table_height]
Kevin Deldycke's avatar
Kevin Deldycke committed
517

518 519 520 521
  def _getTableSizeDict(self, table):
    """
      Get table dimension as dictionnary contain both height and width
    """
Kevin Deldycke's avatar
Kevin Deldycke committed
522
    max_cols = 0
523 524 525 526
    for line_index in range(len(table)):
      line = table[line_index]
      if len(line) > max_cols:
        max_cols = len(line)
Kevin Deldycke's avatar
Kevin Deldycke committed
527

528 529 530
    return { 'width' : max_cols
           , 'height': len(table)
           }
Kevin Deldycke's avatar
Kevin Deldycke committed
531

532
  def _getNormalizedBoundsTable(self, table, width=0, height=0):
Kevin Deldycke's avatar
Kevin Deldycke committed
533
    """
534
      Add necessary cells and lines to obtain given bounds
Kevin Deldycke's avatar
Kevin Deldycke committed
535
    """
536 537
    while height > len(table):
      table.append([])
Kevin Deldycke's avatar
Kevin Deldycke committed
538
    for line in range(height):
539 540 541 542
      while width > len(table[line]):
        table[line].append(None)
    return table

543 544 545 546 547 548 549 550 551 552 553 554 555 556 557
  def _getTableListUnion(self, list1, list2):
    """
      Coerce two dict containing tables structures.
      We need to use this method because a OpenOffice document can hold
        several embedded spreadsheets with the same id. This explain the
        use of random suffix in such extreme case.
    """
    for list2_key in list2.keys():
      # Generate a new table ID if needed
      new_key = list2_key
      while new_key in list1.keys():
        new_key = list2_key + '_' + str(random.randint(1000,9999))
      list1[new_key] = list2[list2_key]
    return list1

Kevin Deldycke's avatar
Kevin Deldycke committed
558
allow_class(OOoParser)
Nicolas Delaby's avatar
Nicolas Delaby committed
559
allow_class(CorruptedOOoFile)
560 561 562 563

def newOOoParser(container):
  return OOoParser().__of__(container)