docs: def transform(odtfile, debug=False, parsable=False, outputdir=None):
      """ Given an ODT file this returns a tuple containing
          the cnxml, a dictionary of filename -> data, and a list of errors """
      # Store mapping of images extracted from the ODT file (and their bits)
      images = {}
      # Log of Errors and Warnings generated
      # For example, the text produced by XSLT should be:
      # {'level':'WARNING',
      #  'msg'  :'Headings without text between them are not allowed',
      #  'id'   :'import-auto-id2376'}
      # That way we can put a little * near all the cnxml where issues arose
      errors = []
  
      zip = zipfile.ZipFile(odtfile, 'r')
      content = zip.read('content.xml')
      xml = etree.fromstring(content)
  
      def appendLog(xslDoc):
          if hasattr(xslDoc, 'error_log'):
              for entry in xslDoc.error_log:
                  # Entries are of the form:
                  # {'level':'ERROR','id':'id1234','msg':'Descriptive message'}
                  text = entry.message
                  try:
                      dict = json.loads(text)
                      errors.append(dict)
                  except ValueError:
                      errors.append({
                        u'level':u'CRITICAL',
                        u'id'   :u'(none)',
                        u'msg'  :unicode(text) })
                      
      def injectStyles(xml):
          # HACK - need to find the object location from the manifest ...
          strStyles = zip.read('styles.xml')
          
          parser = etree.XMLParser()
          parser.feed(strStyles)
          stylesXml = parser.close()
          
          for i, obj in enumerate(STYLES_XPATH(stylesXml)):
            xml.append(obj)
  
          return xml
  
  
      # All MathML is stored in separate files "Object #/content.xml"
      # This converter includes the MathML by looking up the file in the zip
      def mathIncluder(xml):
          for i, obj in enumerate(MATH_XPATH(xml)):
              strMathPath = MATH_HREF_XPATH(obj)[0] # Or obj.get('{%s}href' % XLINK_NS)
              if strMathPath[0] == '#':
                  strMathPath = strMathPath[1:]
              # Remove leading './' Zip doesn't like it
              if strMathPath[0] == '.':
                  strMathPath = strMathPath[2:]
  
              # HACK - need to find the object location from the manifest ...
              strMathPath = os.path.join(strMathPath, 'content.xml')
              strMath = zip.read(strMathPath)
              
              #parser = etree.XMLParser(encoding='utf-8')
              #parser.feed(strMath)
              #math = parser.close()
              math = etree.parse(StringIO(strMath)).getroot()
              
              # Replace the reference to the Math with the actual MathML
              obj.getparent().replace(obj, math)
          return xml
  
      def imagePuller(xml):
          for i, obj in enumerate(IMAGE_XPATH(xml)):
              strPath = IMAGE_HREF_XPATH(obj)[0]
              strName = IMAGE_NAME_XPATH(obj)[0]
  
              fileNeedEnding = ( strName.find('.') == -1 )
              if fileNeedEnding:
                  strName = strName + strPath[strPath.index('.'):]
  
              if strPath[0] == '#':
                  strPath = strPath[1:]
              # Remove leading './' Zip doesn't like it
              if strPath[0] == '.':
                  strPath = strPath[2:]
  
              image = zip.read(strPath)
              images[strName] = image
              
              # Later on, an XSL pass will convert the draw:frame to a c:image and 
              # set the @src correctly
  
          return xml
          
      def drawPuller(xml):
          styles = DRAW_STYLES_XPATH(xml)
                         
          empty_odg_dirname = os.path.join(dirname, 'empty_odg_template')
          
          temp_dirname = tempfile.mkdtemp()
              
          for i, obj in enumerate(DRAW_XPATH(xml)):
              # Copy everything except content.xml from the empty ODG (OOo Draw) template into a new zipfile
              
              odg_filename = DRAW_FILENAME_PREFIX + str(i) + '.odg'
              png_filename = DRAW_FILENAME_PREFIX + str(i) + '.png'
  
              # add PNG filename as attribute to parent node. The good thing is: The child (obj) will get lost! :-)
              parent = obj.getparent()
              parent.attrib['ooo_drawing'] = png_filename
              
              odg_zip = zipfile.ZipFile(os.path.join(temp_dirname, odg_filename), 'w', zipfile.ZIP_DEFLATED)
              for root, dirs, files in os.walk(empty_odg_dirname):
                  for name in files:
                      if name not in ('content.xml', 'styles.xml'):   # copy everything inside ZIP except content.xml or styles.xml
                          sourcename = os.path.join(root, name)
                          # http://stackoverflow.com/a/1193171/756056                        
                          arcname = os.path.join(root[len(empty_odg_dirname):], name)  # Path name inside the ZIP file, empty_odg_template is the root folder
                          odg_zip.write(sourcename, arcname)
              
              content = etree.parse(os.path.join(empty_odg_dirname, 'content.xml'))
                             
              # Inject content styles in empty OOo Draw content.xml
              content_style_xpath = etree.XPath('/office:document-content/office:automatic-styles', namespaces=NAMESPACES)
              content_styles = content_style_xpath(content)                                
              for style in styles:
                  content_styles[0].append(deepcopy(style))
              
              # Inject drawing in empty OOo Draw content.xml
              content_page_xpath = etree.XPath('/office:document-content/office:body/office:drawing/draw:page', namespaces=NAMESPACES)
              content_page = content_page_xpath(content)
              content_page[0].append(obj)
              
              # write modified content.xml
              odg_zip.writestr('content.xml', etree.tostring(content, xml_declaration=True, encoding='UTF-8'))
              
              # copy styles.xml from odt to odg without modification
              styles_xml = zip.read('styles.xml')
              odg_zip.writestr('styles.xml', styles_xml)
  
              odg_zip.close()
              
              # TODO: Better error handling in the future.
              try:
                  # convert every odg to png
                  command = '/usr/bin/soffice -headless -nologo -nofirststartwizard "macro:///Standard.Module1.SaveAsPNG(%s,%s)"' % (os.path.join(temp_dirname, odg_filename),os.path.join(temp_dirname, png_filename))
                  os.system(command)
  
                  # save every image to memory            
                  image = open(os.path.join(temp_dirname, png_filename), 'r').read()
                  images[png_filename] = image
                  
                  if outputdir is not None:
                      shutil.copy (os.path.join(temp_dirname, odg_filename), os.path.join(outputdir, odg_filename))
                      shutil.copy (os.path.join(temp_dirname, png_filename), os.path.join(outputdir, png_filename))
              except:
                  pass
                  
          # delete temporary directory
          shutil.rmtree(temp_dirname)
          
          return xml
  
      # Reparse after XSL because the RED-escape pass injects arbitrary XML
      def redParser(xml):
          xsl = makeXsl('pass1_odt2red-escape.xsl')
          result = xsl(xml)
          appendLog(xsl)
          try:
              xml = etree.fromstring(etree.tostring(result))
          except etree.XMLSyntaxError, e:
              msg = str(e)
              xml = makeXsl('pass1_odt2red-failed.xsl')(xml, message="'%s'" % msg.replace("'", '"'))
              xml = xml.getroot()
          return xml
  
      def replaceSymbols(xml):
          xmlstr = etree.tostring(xml)
          xmlstr = symbols.replace(xmlstr)
          return etree.fromstring(xmlstr)
  
      PIPELINE = [
        drawPuller, # gets OOo Draw objects out of odt and generate odg (OOo Draw) files
        replaceSymbols,
        injectStyles, # include the styles.xml file because it contains list numbering info
        makeXsl('pass2_odt-normalize.xsl'), # This needs to be done 2x to fix headings       
        makeXsl('pass2_odt-normalize.xsl'), # In the worst case all headings are 9 
                              # and need to be 1. See (testbed) southwood__Lesson_2.doc
        makeXsl('pass2_odt-collapse-spans.xsl'), # Collapse adjacent spans (for RED)
        redParser, # makeXsl('pass1_odt2red-escape.xsl'),
        makeXsl('pass4_odt-headers.xsl'),
        imagePuller, # Need to run before math because both have a <draw:image> (see xpath)
        mathIncluder,
        makeXsl('pass7_odt2cnxml.xsl'),
        makeXsl('pass8_cnxml-cleanup.xsl'),
        makeXsl('pass8.5_cnxml-cleanup.xsl'),
        makeXsl('pass9_id-generation.xsl'),
        makeXsl('pass10_processing-instruction-logger.xsl'),
        ]
  
      # "xml" variable gets replaced during each iteration
      passNum = 0
      for xslDoc in PIPELINE:
          if debug: errors.append("DEBUG: Starting pass %d" % passNum)
          xml = xslDoc(xml)
  
          appendLog(xslDoc)
          if outputdir is not None: writeXMLFile(os.path.join(outputdir, 'pass%d.xml' % passNum), xml)
          passNum += 1
  
      # In most cases (EIP) Invalid XML is preferable over valid but Escaped XML
      if not parsable:
        xml = (makeXsl('pass11_red-unescape.xsl'))(xml)
  
      return (xml, images, errors)