Пример #1
0
 def finalize(self):
     '''Re-zip the result and potentially call LibreOffice if target format
        is not among self.templateTypes or if forceOoCall is True.'''
     for innerFile in ('content.xml', 'styles.xml'):
         shutil.copy(os.path.join(self.tempFolder, innerFile),
                     os.path.join(self.unzipFolder, innerFile))
     # Insert dynamic styles
     contentXml = os.path.join(self.unzipFolder, 'content.xml')
     f = open(contentXml, 'r+', encoding='utf-8')
     dynamicStyles = ''.join(self.dynamicStyles)
     content = f.read().replace('<!DYNAMIC_STYLES!>', dynamicStyles)
     f.seek(0)
     f.truncate(0)
     f.write(content)
     f.close()
     # Call the user-defined "finalize" function when present
     if self.finalizeFunction:
         try:
             self.finalizeFunction(self.unzipFolder)
         except Exception as e:
             print((WARNING_FINALIZE_ERROR % str(e)))
     # Re-zip the result, first as an OpenDocument file of the same type as
     # the POD template (odt, ods...)
     resultExt = self.getTemplateType()
     resultName = os.path.join(self.tempFolder, 'result.%s' % resultExt)
     zip(resultName, self.unzipFolder, odf=True)
     resultType = os.path.splitext(self.result)[1].strip('.')
     if (resultType in self.templateTypes) and not self.forceOoCall:
         # Simply move the ODT result to the result
         os.rename(resultName, self.result)
     else:
         if resultType not in FILE_TYPES:
             raise PodError(BAD_RESULT_TYPE % (
                 self.result, FILE_TYPES.keys()))
         # Call LibreOffice to perform the conversion or document update.
         output = self.callLibreOffice(resultName, resultType)
         # I (should) have the result. Move it to the correct name.
         resPrefix = os.path.splitext(resultName)[0]
         if resultType in self.templateTypes:
             # converter.py has (normally!) created a second file
             # suffixed .res.[resultType]
             finalResultName = '%s.res.%s' % (resPrefix, resultType)
             if not os.path.exists(finalResultName):
                 finalResultName = resultName
                 # In this case OO in server mode could not be called to
                 # update indexes, sections, etc.
         else:
             finalResultName = '%s.%s' % (resPrefix, resultType)
         if not os.path.exists(finalResultName):
             raise PodError(CONVERT_ERROR % output)
         os.rename(finalResultName, self.result)
Пример #2
0
 def finalize(self):
     '''Re-zip the result and potentially call LibreOffice if target format
        is not among self.templateTypes or if forceOoCall is True.'''
     for innerFile in ('content.xml', 'styles.xml'):
         shutil.copy(os.path.join(self.tempFolder, innerFile),
                     os.path.join(self.unzipFolder, innerFile))
     # Insert dynamic styles
     contentXml = os.path.join(self.unzipFolder, 'content.xml')
     f = open(contentXml, 'r+', encoding='utf-8')
     dynamicStyles = ''.join(self.dynamicStyles)
     content = f.read().replace('<!DYNAMIC_STYLES!>', dynamicStyles)
     f.seek(0)
     f.truncate(0)
     f.write(content)
     f.close()
     # Call the user-defined "finalize" function when present
     if self.finalizeFunction:
         try:
             self.finalizeFunction(self.unzipFolder)
         except Exception as e:
             print((WARNING_FINALIZE_ERROR % str(e)))
     # Re-zip the result, first as an OpenDocument file of the same type as
     # the POD template (odt, ods...)
     resultExt = self.getTemplateType()
     resultName = os.path.join(self.tempFolder, 'result.%s' % resultExt)
     zip(resultName, self.unzipFolder, odf=True)
     resultType = os.path.splitext(self.result)[1].strip('.')
     if (resultType in self.templateTypes) and not self.forceOoCall:
         # Simply move the ODT result to the result
         os.rename(resultName, self.result)
     else:
         if resultType not in FILE_TYPES:
             raise PodError(BAD_RESULT_TYPE %
                            (self.result, FILE_TYPES.keys()))
         # Call LibreOffice to perform the conversion or document update.
         output = self.callLibreOffice(resultName, resultType)
         # I (should) have the result. Move it to the correct name.
         resPrefix = os.path.splitext(resultName)[0]
         if resultType in self.templateTypes:
             # converter.py has (normally!) created a second file
             # suffixed .res.[resultType]
             finalResultName = '%s.res.%s' % (resPrefix, resultType)
             if not os.path.exists(finalResultName):
                 finalResultName = resultName
                 # In this case OO in server mode could not be called to
                 # update indexes, sections, etc.
         else:
             finalResultName = '%s.%s' % (resPrefix, resultType)
         if not os.path.exists(finalResultName):
             raise PodError(CONVERT_ERROR % output)
         os.rename(finalResultName, self.result)
Пример #3
0
class Renderer:
    templateTypes = ('odt', 'ods') # Types of POD templates

    def __init__(self, template, context, result, pythonWithUnoPath=None,
                 ooPort=2002, stylesMapping={}, forceOoCall=False,
                 finalizeFunction=None, overwriteExisting=False,
                 raiseOnError=False, imageResolver=None):
        '''This Python Open Document Renderer (PodRenderer) loads a document
           template (p_template) which is an ODT or ODS file with some elements
           written in Python. Based on this template and some Python objects
           defined in p_context, the renderer generates an ODT file (p_result)
           that instantiates the p_template and fills it with objects from the
           p_context.

         - If p_result does not end with .odt or .ods, the Renderer will call
           LibreOffice to perform a conversion. If p_forceOoCall is True, even
           if p_result ends with .odt, LibreOffice will be called, not for
           performing a conversion, but for updating some elements like indexes
           (table of contents, etc) and sections containing links to external
           files (which is the case, for example, if you use the default
           function "document").

         - If the Python interpreter which runs the current script is not
           UNO-enabled, this script will run, in another process, a UNO-enabled
           Python interpreter (whose path is p_pythonWithUnoPath) which will
           call LibreOffice. In both cases, we will try to connect to
           LibreOffice in server mode on port p_ooPort.

         - If you plan to make "XHTML to OpenDocument" conversions, you may
           specify a styles mapping in p_stylesMapping.

         - If you specify a function in p_finalizeFunction, this function will
           be called by the renderer before re-zipping the ODT/S result. This
           way, you can still perform some actions on the content of the ODT/S
           file before it is zipped and potentially converted. This function
           must accept one arg: the absolute path to the temporary folder
           containing the un-zipped content of the ODT/S result.

         - If you set p_overwriteExisting to True, the renderer will overwrite
           the result file. Else, an exception will be thrown if the result file
           already exists.

         - If p_raiseOnError is False (the default value), any error encountered
           during the generation of the result file will be dumped into it, as
           a Python traceback within a note. Else, the error will be raised.

         - p_imageResolver allows POD to retrieve images, from "img" tags within
           XHTML content. Indeed, POD may not be able (ie, may not have the
           permission to) perform a HTTP GET on those images. Currently, the
           resolver can only be a Zope application object.
        '''
        self.template = template
        self.templateZip = zipfile.ZipFile(template)
        self.result = result
        self.contentXml = None # Content (string) of content.xml
        self.stylesXml = None # Content (string) of styles.xml
        self.stylesManager = None # Manages the styles defined into the ODT
        # template
        self.tempFolder = None
        self.env = None
        self.pyPath = pythonWithUnoPath
        self.ooPort = ooPort
        self.forceOoCall = forceOoCall
        self.finalizeFunction = finalizeFunction
        self.overwriteExisting = overwriteExisting
        self.raiseOnError = raiseOnError
        self.imageResolver = imageResolver
        # Remember potential files or images that will be included through
        # "do ... from document" statements: we will need to declare them in
        # META-INF/manifest.xml. Keys are file names as they appear within the
        # ODT file (to dump in manifest.xml); values are original paths of
        # included images (used for avoiding to create multiple copies of a file
        # which is imported several times).
        self.fileNames = {}
        self.prepareFolders()
        # Unzip template
        self.unzipFolder = os.path.join(self.tempFolder, 'unzip')
        os.mkdir(self.unzipFolder)
        for zippedFile in self.templateZip.namelist():
            # Before writing the zippedFile into self.unzipFolder, create the
            # intermediary subfolder(s) if needed.
            fileName = None
            if zippedFile.endswith('/') or zippedFile.endswith(os.sep):
                # This is an empty folder. Create it nevertheless. If zippedFile
                # starts with a '/', os.path.join will consider it an absolute
                # path and will throw away self.unzipFolder.
                os.makedirs(os.path.join(self.unzipFolder,
                                         zippedFile.lstrip('/')))
            else:
                fileName = os.path.basename(zippedFile)
                folderName = os.path.dirname(zippedFile)
                fullFolderName = self.unzipFolder
                if folderName:
                    fullFolderName = os.path.join(fullFolderName, folderName)
                    if not os.path.exists(fullFolderName):
                        os.makedirs(fullFolderName)
            # Unzip the file in self.unzipFolder
            if fileName:
                fullFileName = os.path.join(fullFolderName, fileName)
                f = open(fullFileName, 'wb')
                fileContent = self.templateZip.read(zippedFile)
                if (fileName == 'content.xml') and not folderName:
                    # content.xml files may reside in subfolders.
                    # We modify only the one in the root folder.
                    self.contentXml = fileContent
                elif (fileName == 'styles.xml') and not folderName:
                    # Same remark as above.
                    self.stylesManager = StylesManager(fileContent)
                    self.stylesXml = fileContent
                elif (fileName == 'mimetype') and \
                     (fileContent == mimeTypes['ods']):
                    # From LibreOffice 3.5, it is not possible anymore to dump
                    # errors into the resulting ods as annotations. Indeed,
                    # annotations can't reside anymore within paragraphs. ODS
                    # files generated with pod and containing error messages in
                    # annotations cause LibreOffice 3.5 and 4.0 to crash.
                    # LibreOffice >= 4.1 simply does not show the annotation.
                    self.raiseOnError = True
                f.write(fileContent)
                f.close()
        self.templateZip.close()
        # Create the content.xml parser
        pe = PodEnvironment
        contentInserts = (
            OdInsert(CONTENT_POD_FONTS,
                XmlElement('font-face-decls', nsUri=pe.NS_OFFICE),
                nsUris={'style': pe.NS_STYLE, 'svg': pe.NS_SVG}),
            OdInsert(CONTENT_POD_STYLES,
                XmlElement('automatic-styles', nsUri=pe.NS_OFFICE),
                nsUris={'style': pe.NS_STYLE, 'fo': pe.NS_FO,
                        'text': pe.NS_TEXT, 'table': pe.NS_TABLE}))
        self.contentParser = self.createPodParser('content.xml', context,
                                                  contentInserts)
        # Create the styles.xml parser
        stylesInserts = (
            OdInsert(STYLES_POD_FONTS,
                XmlElement('font-face-decls', nsUri=pe.NS_OFFICE),
                nsUris={'style': pe.NS_STYLE, 'svg': pe.NS_SVG}),
            OdInsert(STYLES_POD_STYLES,
                XmlElement('styles', nsUri=pe.NS_OFFICE),
                nsUris={'style': pe.NS_STYLE, 'fo': pe.NS_FO,
                        'text': pe.NS_TEXT}))
        self.stylesParser = self.createPodParser('styles.xml', context,
                                                 stylesInserts)
        # Store the styles mapping
        self.setStylesMapping(stylesMapping)
        # While working, POD may identify "dynamic styles" to insert into
        # the "automatic styles" section of content.xml, like the column styles
        # of tables generated from XHTML tables via xhtml2odt.py.
        self.dynamicStyles = []

    def createPodParser(self, odtFile, context, inserts):
        '''Creates the parser with its environment for parsing the given
           p_odtFile (content.xml or styles.xml). p_context is given by the pod
           user, while p_inserts depends on the ODT file we must parse.'''
        evalContext = {'xhtml': self.renderXhtml,
                       'text':  self.renderText,
                       'test': self.evalIfExpression,
                       'document': self.importDocument,
                       'pod': self.importPod,
                       'pageBreak': self.insertPageBreak} # Default context
        if hasattr(context, '__dict__'):
            evalContext.update(context.__dict__)
        elif isinstance(context, dict) or isinstance(context, UserDict):
            evalContext.update(context)
        else:
            raise PodError(BAD_CONTEXT)
        env = PodEnvironment(evalContext, inserts)
        fileBuffer = FileBuffer(env, os.path.join(self.tempFolder,odtFile))
        env.currentBuffer = fileBuffer
        return PodParser(env, self)

    def renderXhtml(self, xhtmlString, encoding='utf-8', stylesMapping={}):
        '''Method that can be used (under the name 'xhtml') into a pod template
           for converting a chunk of XHTML content (p_xhtmlString) into a chunk
           of ODT content.'''
        stylesMapping = self.stylesManager.checkStylesMapping(stylesMapping)
        # xhtmlString can only be a chunk of XHTML. So we must surround it with
        # a tag in order to get a XML-compliant file (we need a root tag).
        if xhtmlString == None: xhtmlString = ''
        xhtmlContent = '<p>%s</p>' % xhtmlString
        return Xhtml2OdtConverter(xhtmlContent, encoding, self.stylesManager,
                                  stylesMapping, self).run()

    def renderText(self, text, encoding='utf-8', stylesMapping={}):
        '''Obsolete method.'''
        raise Exception(OBSOLETE_RENDER_TEXT)

    def evalIfExpression(self, condition, ifTrue, ifFalse):
        '''This method implements the method 'test' which is proposed in the
           default pod context. It represents an 'if' expression (as opposed to
           the 'if' statement): depending on p_condition, expression result is
           p_ifTrue or p_ifFalse.'''
        if condition:
            return ifTrue
        return ifFalse

    imageFormats = ('png', 'jpeg', 'jpg', 'gif', 'svg')
    ooFormats = ('odt',)
    convertibleFormats = FILE_TYPES.keys()
    def importDocument(self, content=None, at=None, format=None,
                       anchor='as-char', wrapInPara=True, size=None,
                       sizeUnit='cm', style=None,
                       pageBreakBefore=False, pageBreakAfter=False):
        '''If p_at is not None, it represents a path or url allowing to find
           the document. If p_at is None, the content of the document is
           supposed to be in binary format in p_content. The document
           p_format may be: odt or any format in imageFormats.

           p_anchor, p_wrapInPara and p_size, p_sizeUnit and p_style are only
           relevant for images:
           * p_anchor defines the way the image is anchored into the document;
                      Valid values are 'page','paragraph', 'char' and 'as-char';
           * p_wrapInPara, if true, wraps the resulting 'image' tag into a 'p'
                           tag;
           * p_size, if specified, is a tuple of float or integers
                     (width, height) expressing size in p_sizeUnit (see below).
                     If not specified, size will be computed from image info;
           * p_sizeUnit is the unit for p_size elements, it can be "cm"
             (centimeters) or "px" (pixels);
           * if p_style is given, it is the content of a "style" attribute,
             containing CSS attributes. If "width" and "heigth" attributes are
             found there, they will override p_size and p_sizeUnit.

           p_pageBreakBefore and p_pageBreakAfter are only relevant for import
           of external odt documents, and allows to insert a page break
           before/after the inserted document.
        '''
        importer = None
        # Is there someting to import?
        if not content and not at: raise PodError(DOC_NOT_SPECIFIED)
        # Convert Zope files into Appy wrappers.
        if content.__class__.__name__ in ('File', 'Image'):
            content = FileWrapper(content)
        # Guess document format
        if isinstance(content, FileWrapper):
            format = content.mimeType
        if not format:
            # It should be deduced from p_at
            if not at:
                raise PodError(DOC_FORMAT_ERROR)
            format = os.path.splitext(at)[1][1:]
        else:
            # If format is a mimeType, convert it to an extension
            if mimeTypesExts.has_key(format):
                format = mimeTypesExts[format]
        isImage = False
        isOdt = False
        if format in self.ooFormats:
            importer = OdtImporter
            self.forceOoCall = True
            isOdt = True
        elif (format in self.imageFormats) or not format:
            # If the format can't be guessed, we suppose it is an image.
            importer = ImageImporter
            isImage = True
        elif format == 'pdf':
            importer = PdfImporter
        elif format in self.convertibleFormats:
            importer = ConvertImporter
        else:
            raise PodError(DOC_WRONG_FORMAT % format)
        imp = importer(content, at, format, self)
        # Initialise image-specific parameters
        if isImage: imp.init(anchor, wrapInPara, size, sizeUnit, style)
        elif isOdt: imp.init(pageBreakBefore, pageBreakAfter)
        return imp.run()

    def importPod(self, content=None, at=None, format='odt', context=None,
                  pageBreakBefore=False, pageBreakAfter=False):
        '''Similar to m_importDocument, but allows to import the result of
           executing the POD template specified in p_content or p_at, and
           include it in the POD result.'''
        # Is there a pod template defined?
        if not content and not at:
            raise PodError(DOC_NOT_SPECIFIED)
        # If the POD template is specified as a Zope file, convert it into a
        # Appy FileWrapper.
        if content.__class__.__name__ == 'File':
            content = FileWrapper(content)
        imp = PodImporter(content, at, format, self)
        self.forceOoCall = True
        # Define the context to use: either the current context of the current
        # POD renderer, or p_context if given.
        if context:
            ctx = context
        else:
            ctx = self.contentParser.env.context
        imp.init(ctx, pageBreakBefore, pageBreakAfter)
        return imp.run()

    def insertPageBreak(self):
        '''Inserts a page break into the result.'''
        textNs = self.currentParser.env.namespaces[PodEnvironment.NS_TEXT]
        return '<%s:p %s:style-name="podPageBreak"></%s:p>' % \
               (textNs, textNs, textNs)

    def prepareFolders(self):
        # Check if I can write the result
        if not self.overwriteExisting and os.path.exists(self.result):
            raise PodError(RESULT_FILE_EXISTS % self.result)
        try:
            f = open(self.result, 'w')
            f.write('Hello')
            f.close()
        except OSError, oe:
            raise PodError(CANT_WRITE_RESULT % (self.result, oe))
        except IOError, ie:
            raise PodError(CANT_WRITE_RESULT % (self.result, ie))
Пример #4
0
     if not dirnames and not filenames:
         # This is an empty leaf folder. We must create an entry in the
         # zip for him.
         folderName = dir[len(self.unzipFolder):]
         zInfo = zipfile.ZipInfo("%s/" % folderName,time.localtime()[:6])
         zInfo.external_attr = 48
         resultZip.writestr(zInfo, '')
 resultZip.close()
 resultType = os.path.splitext(self.result)[1].strip('.')
 if (resultType in self.templateTypes) and not self.forceOoCall:
     # Simply move the ODT result to the result
     os.rename(resultName, self.result)
 else:
     if resultType not in FILE_TYPES:
         raise PodError(BAD_RESULT_TYPE % (
             self.result, FILE_TYPES.keys()))
     # Call LibreOffice to perform the conversion or document update.
     output = self.callLibreOffice(resultName, resultType)
     # I (should) have the result. Move it to the correct name.
     resPrefix = os.path.splitext(resultName)[0]
     if resultType in self.templateTypes:
         # converter.py has (normally!) created a second file
         # suffixed .res.[resultType]
         finalResultName = '%s.res.%s' % (resPrefix, resultType)
         if not os.path.exists(finalResultName):
             finalResultName = resultName
             # In this case OO in server mode could not be called to
             # update indexes, sections, etc.
     else:
         finalResultName = '%s.%s' % (resPrefix, resultType)
     if not os.path.exists(finalResultName):
Пример #5
0
     if not dirnames and not filenames:
         # This is an empty leaf folder. We must create an entry in the
         # zip for him
         folderName = dir[len(self.unzipFolder):]
         zInfo = zipfile.ZipInfo("%s/" % folderName,time.localtime()[:6])
         zInfo.external_attr = 48
         resultOdt.writestr(zInfo, '')
 resultOdt.close()
 resultType = os.path.splitext(self.result)[1]
 try:
     if (resultType == '.odt') and not self.forceOoCall:
         # Simply move the ODT result to the result
         os.rename(resultOdtName, self.result)
     else:
         if resultType.startswith('.'): resultType = resultType[1:]
         if not resultType in FILE_TYPES.keys():
             raise PodError(BAD_RESULT_TYPE % (
                 self.result, FILE_TYPES.keys()))
         # Call OpenOffice to perform the conversion or document update
         print 'At this point, %s must exist'% resultOdtName
         assert os.path.exists(resultOdtName)
         output = self.callOpenOffice(resultOdtName, resultType)
         # I (should) have the result. Move it to the correct name
         resPrefix = os.path.splitext(resultOdtName)[0] + '.'
         if resultType == 'odt':
             # converter.py has (normally!) created a second file
             # suffixed .res.odt
             resultName = resPrefix + 'res.odt'
             if not os.path.exists(resultName):
                 resultName = resultOdtName
                 # In this case OO in server mode could not be called to
Пример #6
0
class Renderer:
    templateTypes = ('odt', 'ods') # Types of POD templates

    def __init__(self, template, context, result, pythonWithUnoPath=None,
      ooPort=2002, stylesMapping={}, forceOoCall=False, finalizeFunction=None,
      overwriteExisting=False, raiseOnError=False, imageResolver=None,
      stylesTemplate=None, optimalColumnWidths=False, script=None,
      renamePageStyles=False):
        '''This Python Open Document Renderer (PodRenderer) loads a document
           template (p_template) which is an ODT or ODS file with some elements
           written in Python. Based on this template and some Python objects
           defined in p_context, the renderer generates an ODT file (p_result)
           that instantiates the p_template and fills it with objects from the
           p_context.

         - If p_result does not end with .odt or .ods, the Renderer will call
           LibreOffice to perform a conversion. If p_forceOoCall is True, even
           if p_result ends with .odt, LibreOffice will be called, not for
           performing a conversion, but for updating some elements like indexes
           (table of contents, etc) and sections containing links to external
           files (which is the case, for example, if you use the default
           function "document").

         - If the Python interpreter which runs the current script is not
           UNO-enabled, this script will run, in another process, a UNO-enabled
           Python interpreter (whose path is p_pythonWithUnoPath) which will
           call LibreOffice. In both cases, we will try to connect to
           LibreOffice in server mode on port p_ooPort.

         - If you plan to make "XHTML to OpenDocument" conversions (via the POD
           function "xhtml"), you may specify a styles mapping in
           p_stylesMapping.

         - If you specify a function in p_finalizeFunction, this function will
           be called by the renderer before re-zipping the ODT/S result. This
           way, you can still perform some actions on the content of the ODT/S
           file before it is zipped and potentially converted. This function
           must accept 2 args:
            * the absolute path to the temporary folder, containing the
              un-zipped content of the ODT/S result;
            * the Renderer instance.

         - If you set p_overwriteExisting to True, the renderer will overwrite
           the result file. Else, an exception will be thrown if the result file
           already exists.

         - If p_raiseOnError is False (the default value), any error encountered
           during the generation of the result file will be dumped into it, as
           a Python traceback within a note. Else, the error will be raised.

         - p_imageResolver allows POD to retrieve images, from "img" tags within
           XHTML content. Indeed, POD may not be able (ie, may not have the
           permission to) perform a HTTP GET on those images. Currently, the
           resolver can only be a Zope application object.

         - p_stylesTemplate can be the path to a LibreOffice file (ie, a .ott
           file) whose styles will be imported within the result.

         - p_optimalColumnWidths corresponds to the homonym option to
           converter.py, excepted that values "True" or "False" must be boolean
           values. Note that the POD function "xhtml" requires this parameter to
           be "OCW_.*" to be fully operational. When optimalColumnWidths is not
           False, forceOoCall is forced to True.

         - p_script is the absolute path to a Python script containing functions
           that the converter will call in order to customize the process of
           manipulating the document via the LibreOffice UNO interface. For more
           information, see appy/pod/converter.py, option "-s". Note that when
           such p_script is specified, p_forceOoCall is forced to True.

         - If this document is a sub-document to be included in a master one, it
           has sense to set parameter p_renamePageStyles to True. This way, when
           importing it into the master document, all page styles will be
           considered different and tied elements like headers and footers will
           correctly be imported into the master document. The "do... pod"
           statement automatically sets this parameter to True.
        '''
        self.template = template
        self.result = result
        self.contentXml = None # Content (string) of content.xml
        self.stylesXml = None # Content (string) of styles.xml
        self.stylesManager = None # Manages the styles defined into the ODT
        # template
        self.tempFolder = None
        self.env = None
        self.pyPath = pythonWithUnoPath
        self.ooPort = ooPort
        # p_forceOoCall may be forced to True
        self.forceOoCall = forceOoCall or \
                           bool(optimalColumnWidths) or bool(script)
        self.finalizeFunction = finalizeFunction
        self.overwriteExisting = overwriteExisting
        self.raiseOnError = raiseOnError
        self.imageResolver = imageResolver
        self.stylesTemplate = stylesTemplate
        self.optimalColumnWidths = optimalColumnWidths
        self.script = script
        self.renamePageStyles = renamePageStyles
        # Keep trace of the original context given to the renderer
        self.originalContext = context
        # Remember potential files or images that will be included through
        # "do ... from document" statements: we will need to declare them in
        # META-INF/manifest.xml. Keys are file names as they appear within the
        # ODT file (to dump in manifest.xml); values are original paths of
        # included images (used for avoiding to create multiple copies of a file
        # which is imported several times).
        self.fileNames = {}
        self.prepareFolders()
        # Unzip template
        self.unzipFolder = os.path.join(self.tempFolder, 'unzip')
        os.mkdir(self.unzipFolder)
        info = unzip(template, self.unzipFolder, odf=True)
        self.contentXml = info['content.xml']
        self.stylesXml = info['styles.xml']
        self.stylesManager = StylesManager(self)
        # From LibreOffice 3.5, it is not possible anymore to dump errors into
        # the resulting ods as annotations. Indeed, annotations can't reside
        # anymore within paragraphs. ODS files generated with pod and containing
        # error messages in annotations cause LibreOffice 3.5 and 4.0 to crash.
        # LibreOffice >= 4.1 simply does not show the annotation.
        if info['mimetype'] == utils.mimeTypes['ods']: self.raiseOnError = True
        # Create the parsers for content.xml and styles.xml
        nso = PodEnvironment.NS_OFFICE
        for name in ('content', 'styles'):
            styleTag = (name == 'content') and 'automatic-styles' or 'styles'
            inserts = (
              OdInsert(POD_FONTS, XmlElement('font-face-decls', nsUri=nso)),
              OdInsert(POD_STYLES[name], XmlElement(styleTag, nsUri=nso)))
            parser = self.createPodParser('%s.xml' % name, context, inserts)
            setattr(self, '%sParser' % name, parser)
        # Store the styles mapping
        self.setStylesMapping(stylesMapping)
        # While working, POD may identify "dynamic styles" to insert either in
        # the "automatic-styles" section of content.xml (ie, the column styles
        # of tables generated from XHTML tables via xhtml2odt.py), or in the
        # "styles" section of styles.xml (ie, bullet styles).
        self.dynamicStyles = {'content': [], 'styles': []}

    def createPodParser(self, odtFile, context, inserts=None):
        '''Creates the parser with its environment for parsing the given
           p_odtFile (content.xml or styles.xml). p_context is given by the pod
           user, while p_inserts depends on the ODT file we must parse.'''
        # The default evaluation context
        evalContext = {}
        if hasattr(context, '__dict__'):
            evalContext.update(context.__dict__)
        elif isinstance(context, dict) or isinstance(context, UserDict):
            evalContext.update(context)
        else:
            raise PodError(BAD_CONTEXT)
        # Incorporate the default, unalterable, context
        evalContext.update({'xhtml': self.renderXhtml,
          'test': self.evalIfExpression, 'document': self.importDocument,
          'pod': self.importPod, 'TableProperties': TableProperties,
          'BulletedProperties': BulletedProperties,
          'NumberedProperties': NumberedProperties,
          'pageBreak': self.insertPageBreak,
          'columnBreak': self.insertColumnBreak,
          # Variables to use for representing pod-reserved chars
          'PIPE': '|', 'SEMICOLON': ';'})
        # Developer, forget the following line
        if '_ctx_' not in evalContext: evalContext['_ctx_'] = evalContext
        env = PodEnvironment(evalContext, inserts)
        fileBuffer = FileBuffer(env, os.path.join(self.tempFolder,odtFile))
        env.currentBuffer = fileBuffer
        return PodParser(env, self)

    def renderXhtml(self, s, encoding='utf-8', stylesMapping={},keepWithNext=0):
        '''Method that can be used (under the name 'xhtml') into a pod template
           for converting a chunk of XHTML content (p_xhtmlString) into a chunk
           of ODT content.'''
        stylesMapping = self.stylesManager.checkStylesMapping(stylesMapping)
        # xhtmlString can only be a chunk of XHTML. So we must surround it with
        # a tag in order to get a XML-compliant file (we need a root tag). We
        # also remove special blank chars that produce SAX parsing errors.
        if s == None: s = ''
        # if isinstance(s, unicode): s = s.encode('utf-8')
        s = '<p>%s</p>' % s.replace('\f', '').replace('\v', '')
        # This also removes problematic chars "\x0c" and "\x0b" which simply are
        # alternative ways to write (respectively) "\f" and "\v".
        return Xhtml2OdtConverter(s, encoding, self.stylesManager,
                                  stylesMapping, keepWithNext, self).run()

    def evalIfExpression(self, condition, ifTrue, ifFalse):
        '''This method implements the method 'test' which is proposed in the
           default pod context. It represents an 'if' expression (as opposed to
           the 'if' statement): depending on p_condition, expression result is
           p_ifTrue or p_ifFalse.'''
        if condition: return ifTrue
        return ifFalse

    # Supported image formats. "image" represents any format
    imageFormats = ('png', 'jpeg', 'jpg', 'gif', 'svg', 'image')
    ooFormats = ('odt',)
    convertibleFormats = FILE_TYPES.keys()

    def importDocument(self, content=None, at=None, format=None,
      anchor='as-char', wrapInPara=True, size=None, sizeUnit='cm', style=None,
      keepRatio=True, pageBreakBefore=False, pageBreakAfter=False,
      convertOptions=None):
        '''If p_at is not None, it represents a path or url allowing to find
           the document. If p_at is None, the content of the document is
           supposed to be in binary format in p_content. The document
           p_format may be: odt or any format in imageFormats.

           p_anchor, p_wrapInPara, p_size, p_sizeUnit, p_style and p_keepRatio
           are only relevant for images:
           * p_anchor defines the way the image is anchored into the document;
                      Valid values are 'page','paragraph', 'char' and 'as-char';
           * p_wrapInPara, if true, wraps the resulting 'image' tag into a 'p'
                           tag;
           * p_size, if specified, is a tuple of float or integers
                     (width, height) expressing size in p_sizeUnit (see below).
                     If not specified, size will be computed from image info;
           * p_sizeUnit is the unit for p_size elements, it can be "cm"
             (centimeters), "px" (pixels) or "pc" (percentage). Percentages, in
             p_size, must be expressed as integers from 1 to 100.
           * if p_style is given, it a appy.shared.css.CssStyles instance,
             containing CSS attributes. If "width" and "heigth" attributes are
             found there, they will override p_size and p_sizeUnit.
           * If p_keepRatio is True, the image width/height ratio will be kept
             when p_size is specified.

           p_pageBreakBefore and p_pageBreakAfter are only relevant for
           importing external odt documents, and allows to insert a page break
           before/after the inserted document. More precisely, each of these
           parameters can have values:
           * True     insert a page break;
           * False    do no insert a page break;

           moreover, p_pageBreakAfter can have this additional parameter:
           * 'duplex' insert 2 page breaks if the sub-document has an odd number
                      of pages, 1 else (useful for duplex printing).

           If p_convertOptions are given (for images only), imagemagick will be
           called with these options to perform some transformation on the
           image. For example, if you specify
                             convertOptions="-rotate 90"

           pod will perform this command before importing the file into the
           result:
                       convert your.image -rotate 90 your.image

           You can also specify a function in convertOptions. This function will
           receive a single arg, "image", an instance of
           appy.pod.doc_importers.Image giving some characteristics of the image
           to convert, like image.width and image.height in pixels (integers).
           If your function does not return a string containing the convert
           options, no conversion will occur.
        '''
        importer = None
        # Is there someting to import ?
        if not content and not at: raise PodError(DOC_NOT_SPECIFIED)
        # Convert Zope files into Appy wrappers
        if content.__class__.__name__ in ('File', 'Image'):
            content = utils.FileWrapper(content)
        # Guess document format
        if isinstance(content, utils.FileWrapper):
            format = content.mimeType
        if not format:
            # It should be deduced from p_at
            if not at:
                raise PodError(DOC_FORMAT_ERROR)
            format = os.path.splitext(at)[1][1:]
        else:
            # If format is a mimeType, convert it to an extension
            if format in utils.mimeTypesExts:
                format = utils.mimeTypesExts[format]
        isImage = False
        isOdt = False
        if format in self.ooFormats:
            importer = OdtImporter
            self.forceOoCall = True
            isOdt = True
        elif (format in self.imageFormats) or not format:
            # If the format can't be guessed, we suppose it is an image
            importer = ImageImporter
            isImage = True
        elif format == 'pdf':
            importer = PdfImporter
        elif format in self.convertibleFormats:
            importer = ConvertImporter
        else:
            raise PodError(DOC_WRONG_FORMAT % format)
        imp = importer(content, at, format, self)
        # Initialise image-specific parameters
        if isImage:
            imp.init(anchor, wrapInPara, size, sizeUnit, style, keepRatio,
                     convertOptions)
        elif isOdt: imp.init(pageBreakBefore, pageBreakAfter)
        return imp.run()

    def getResolvedNamespaces(self):
        '''Gets a context where mainly used namespaces have been resolved'''
        env = self.stylesParser.env
        return {'text': env.ns(env.NS_TEXT), 'style': env.ns(env.NS_STYLE)}

    def importPod(self, content=None, at=None, format='odt', context=None,
                  pageBreakBefore=False, pageBreakAfter=False):
        '''Similar to m_importDocument, but allows to import the result of
           executing the POD template specified in p_content or p_at, and
           include it in the POD result.'''
        # Is there a pod template defined ?
        if not content and not at:
            raise PodError(DOC_NOT_SPECIFIED)
        # If the POD template is specified as a Zope file, convert it into a
        # Appy FileWrapper.
        if content.__class__.__name__ == 'File':
            content = utils.FileWrapper(content)
        imp = PodImporter(content, at, format, self)
        self.forceOoCall = True
        # Define the context to use: either the current context of the current
        # POD renderer, or p_context if given.
        if context:
            ctx = context
        else:
            ctx = self.contentParser.env.context
        imp.init(ctx, pageBreakBefore, pageBreakAfter)
        return imp.run()

    def _insertBreak(self, type):
        '''Inserts a page or column break into the result'''
        return '<text:p text:style-name="pod%sBreak"></text:p>' % \
               type.capitalize()
    def insertPageBreak(self): return self._insertBreak('page')
    def insertColumnBreak(self): return self._insertBreak('column')

    def prepareFolders(self):
        # Check if I can write the result
        if not self.overwriteExisting and os.path.exists(self.result):
            raise PodError(RESULT_FILE_EXISTS % self.result)
        try:
            f = open(self.result, 'w')
            f.write('Hello')
            f.close()
        except OSError as oe:
            raise PodError(CANT_WRITE_RESULT % (self.result, oe))
        except IOError as ie:
            raise PodError(CANT_WRITE_RESULT % (self.result, ie))
        self.result = os.path.abspath(self.result)
        os.remove(self.result)
        # Create a temp folder for storing temporary files
        absResult = os.path.abspath(self.result)
        self.tempFolder = '%s.%f' % (absResult, time.time())
        try:
            os.mkdir(self.tempFolder)
        except OSError as oe:
            raise PodError(CANT_WRITE_TEMP_FOLDER % (self.result, oe))

    def patchManifest(self):
        '''Declares, in META-INF/manifest.xml, images or files included via the
           "do... from document" statements if any.'''
        if self.fileNames:
            j = os.path.join
            toInsert = ''
            for fileName in self.fileNames.keys():
                if fileName.endswith('.svg'):
                    fileName = os.path.splitext(fileName)[0] + '.png'
                mimeType = mimetypes.guess_type(fileName)[0]
                toInsert += ' <manifest:file-entry manifest:media-type="%s" ' \
                            'manifest:full-path="%s"/>\n' % (mimeType, fileName)
            manifestName = j(self.unzipFolder, j('META-INF', 'manifest.xml'))
            f = open(manifestName)
            manifestContent = f.read()
            hook = '</manifest:manifest>'
            manifestContent = manifestContent.replace(hook, toInsert+hook)
            f.close()
            # Write the new manifest content
            f = open(manifestName, 'w')
            f.write(manifestContent)
            f.close()

    # Public interface
    def run(self):
        '''Renders the result'''
        try:
            # Remember which parser is running
            self.currentParser = self.contentParser
            # Create the resulting content.xml
            self.currentParser.parse(self.contentXml)
            self.currentParser = self.stylesParser
            # Create the resulting styles.xml
            self.currentParser.parse(self.stylesXml)
            # Patch META-INF/manifest.xml
            self.patchManifest()
            # Re-zip the result
            self.finalize()
        finally:
            FolderDeleter.delete(self.tempFolder)

    def getStyles(self):
        '''Returns a dict of the styles that are defined into the template.'''
        return self.stylesManager.styles

    def setStylesMapping(self, stylesMapping):
        '''Establishes a correspondence between, on one hand, CSS styles or
           XHTML tags that will be found inside XHTML content given to POD,
           and, on the other hand, ODT styles found into the template.'''
        try:
            manager = self.stylesManager
            # Initialise the styles mapping when relevant
            ocw = self.optimalColumnWidths
            if ocw: TableProperties.initStylesMapping(stylesMapping, ocw)
            manager.stylesMapping = manager.checkStylesMapping(stylesMapping)
        except PodError as po:
            self.contentParser.env.currentBuffer.content.close()
            self.stylesParser.env.currentBuffer.content.close()
            if os.path.exists(self.tempFolder):
                FolderDeleter.delete(self.tempFolder)
            raise po

    def callLibreOffice(self, resultName, resultType):
        '''Call LibreOffice in server mode to convert or update the result'''
        loOutput = ''
        try:
            if (not isinstance(self.ooPort, int)) and \
               (not isinstance(self.ooPort, long)):
                raise PodError(BAD_OO_PORT % str(self.ooPort))
            try:
                from appy.pod.converter import Converter, ConverterError
                try:
                    Converter(resultName, resultType, self.ooPort,
                              self.stylesTemplate, self.optimalColumnWidths,
                              self.script).run()
                except ConverterError as ce:
                    raise PodError(CONVERT_ERROR % str(ce))
            except ImportError:
                # I do not have UNO. So try to launch a UNO-enabled Python
                # interpreter which should be in self.pyPath.
                if not self.pyPath:
                    raise PodError(NO_PY_PATH % resultType)
                if not os.path.isfile(self.pyPath):
                    raise PodError(PY_PATH_NOT_FILE % self.pyPath)
                convScript = '%s/converter.py' % \
                            os.path.dirname(appy.pod.__file__)
                cmd = [self.pyPath, convScript, resultName, resultType,
                       '-p%d' % self.ooPort]
                if self.stylesTemplate:
                    cmd.append('-t%s' % self.stylesTemplate)
                if self.optimalColumnWidths:
                    cmd.append('-o')
                    cmd.append('%s' % str(self.optimalColumnWidths))
                if self.script:
                    cmd.append('-s')
                    cmd.append('%s' % self.script)
                out, loOutput = utils.executeCommand(cmd)
        except PodError as pe:
            # When trying to call LO in server mode for producing ODT or ODS
            # (=forceOoCall=True), if an error occurs we have nevertheless
            # an ODT or ODS to return to the user. So we produce a warning
            # instead of raising an error.
            if (resultType in self.templateTypes) and self.forceOoCall:
                print(WARNING_INCOMPLETE_OD % str(pe))
            else:
                raise pe
        return loOutput

    def getTemplateType(self):
        '''Identifies the type of the pod template in self.template
           (ods or odt). If self.template is a string, it is a file name and we
           simply get its extension. Else, it is a binary file in a StringIO
           instance, and we seek the mime type from the first bytes.'''
        if isinstance(self.template, str):
            res = os.path.splitext(self.template)[1][1:]
        else:
            # A StringIO instance
            self.template.seek(0)
            firstBytes = self.template.read(90)
            firstBytes = firstBytes[firstBytes.index('mimetype')+8:]
            if firstBytes.startswith(utils.mimeTypes['ods']):
                res = 'ods'
            else:
                res = 'odt' # We suppose this is ODT
        return res

    def finalize(self):
        '''Re-zip the result and potentially call LibreOffice if target format
           is not among self.templateTypes or if forceOoCall is True.'''
        j = os.path.join
        # If page styles must be renamed, create a dict mapping old > new names
        pageStyles = None
        if self.renamePageStyles:
            pageStyles = {}
            for name in self.stylesManager.stylesParser.env.pageStyleNames:
                pageStyles[name] = 'S%s' % getUuid(removeDots=True)
        for name in ('content', 'styles'):
            # Copy the [content|styles].xml file from the temp to the zip folder
            fn = '%s.xml' % name
            shutil.copy(j(self.tempFolder, fn), j(self.unzipFolder, fn))
            # For styles.xml, complete dynamic styles with default styles for
            # bulleted and numbered lists.
            ds = self.dynamicStyles[name]
            if name == 'styles':
                env = self.stylesParser.env
                n = {'text': env.ns(env.NS_TEXT), 'style': env.ns(env.NS_STYLE)}
                ds.insert(0,NumberedProperties().dumpStyle('podNumberedList',n))
                ds.insert(0,BulletedProperties().dumpStyle('podBulletedList',n))
            # Get the file content and inject dynamic styles into it
            fn = os.path.join(self.unzipFolder, fn)
            f = open(fn)
            content = f.read().replace('<!DYNAMIC_STYLES!>',
                                       b''.join(ds).decode())
            # Rename the page styles
            if pageStyles:
                part = PAGE_STYLES_PARTS[name]
                for old, new in pageStyles.iteritems():
                    content = content.replace(part % old, part % new)
            f.close()
            # Write the updated content to the file
            f = open(fn, 'w')
            f.write(content)
            f.close()
        # Call the user-defined "finalize" function when present
        if self.finalizeFunction:
            try:
                self.finalizeFunction(self.unzipFolder, self)
            except Exception as e:
                print(WARNING_FINALIZE_ERROR % str(e))
        # Re-zip the result, first as an OpenDocument file of the same type as
        # the POD template (odt, ods...)
        resultExt = self.getTemplateType()
        resultName = os.path.join(self.tempFolder, 'result.%s' % resultExt)
        zip(resultName, self.unzipFolder, odf=True)
        resultType = os.path.splitext(self.result)[1].strip('.')
        if (resultType in self.templateTypes) and not self.forceOoCall:
            # Simply move the ODT result to the result
            os.rename(resultName, self.result)
        else:
            if resultType not in FILE_TYPES:
                raise PodError(BAD_RESULT_TYPE % (
                    self.result, FILE_TYPES.keys()))
            # Call LibreOffice to perform the conversion or document update
            output = self.callLibreOffice(resultName, resultType)
            # I (should) have the result. Move it to the correct name.
            resPrefix = os.path.splitext(resultName)[0]
            if resultType in self.templateTypes:
                # converter.py has (normally!) created a second file
                # suffixed .res.[resultType]
                finalResultName = '%s.res.%s' % (resPrefix, resultType)
                if not os.path.exists(finalResultName):
                    finalResultName = resultName
                    # In this case OO in server mode could not be called to
                    # update indexes, sections, etc.
            else:
                finalResultName = '%s.%s' % (resPrefix, resultType)
            if not os.path.exists(finalResultName):
                raise PodError(CONVERT_ERROR % output)
            os.rename(finalResultName, self.result)
Пример #7
0
 def finalize(self):
     '''Re-zip the result and potentially call LibreOffice if target format
        is not among self.templateTypes or if forceOoCall is True.'''
     j = os.path.join
     # If page styles must be renamed, create a dict mapping old > new names
     pageStyles = None
     if self.renamePageStyles:
         pageStyles = {}
         for name in self.stylesManager.stylesParser.env.pageStyleNames:
             pageStyles[name] = 'S%s' % getUuid(removeDots=True)
     for name in ('content', 'styles'):
         # Copy the [content|styles].xml file from the temp to the zip folder
         fn = '%s.xml' % name
         shutil.copy(j(self.tempFolder, fn), j(self.unzipFolder, fn))
         # For styles.xml, complete dynamic styles with default styles for
         # bulleted and numbered lists.
         ds = self.dynamicStyles[name]
         if name == 'styles':
             env = self.stylesParser.env
             n = {'text': env.ns(env.NS_TEXT), 'style': env.ns(env.NS_STYLE)}
             ds.insert(0,NumberedProperties().dumpStyle('podNumberedList',n))
             ds.insert(0,BulletedProperties().dumpStyle('podBulletedList',n))
         # Get the file content and inject dynamic styles into it
         fn = os.path.join(self.unzipFolder, fn)
         f = open(fn)
         content = f.read().replace('<!DYNAMIC_STYLES!>',
                                    b''.join(ds).decode())
         # Rename the page styles
         if pageStyles:
             part = PAGE_STYLES_PARTS[name]
             for old, new in pageStyles.iteritems():
                 content = content.replace(part % old, part % new)
         f.close()
         # Write the updated content to the file
         f = open(fn, 'w')
         f.write(content)
         f.close()
     # Call the user-defined "finalize" function when present
     if self.finalizeFunction:
         try:
             self.finalizeFunction(self.unzipFolder, self)
         except Exception as e:
             print(WARNING_FINALIZE_ERROR % str(e))
     # Re-zip the result, first as an OpenDocument file of the same type as
     # the POD template (odt, ods...)
     resultExt = self.getTemplateType()
     resultName = os.path.join(self.tempFolder, 'result.%s' % resultExt)
     zip(resultName, self.unzipFolder, odf=True)
     resultType = os.path.splitext(self.result)[1].strip('.')
     if (resultType in self.templateTypes) and not self.forceOoCall:
         # Simply move the ODT result to the result
         os.rename(resultName, self.result)
     else:
         if resultType not in FILE_TYPES:
             raise PodError(BAD_RESULT_TYPE % (
                 self.result, FILE_TYPES.keys()))
         # Call LibreOffice to perform the conversion or document update
         output = self.callLibreOffice(resultName, resultType)
         # I (should) have the result. Move it to the correct name.
         resPrefix = os.path.splitext(resultName)[0]
         if resultType in self.templateTypes:
             # converter.py has (normally!) created a second file
             # suffixed .res.[resultType]
             finalResultName = '%s.res.%s' % (resPrefix, resultType)
             if not os.path.exists(finalResultName):
                 finalResultName = resultName
                 # In this case OO in server mode could not be called to
                 # update indexes, sections, etc.
         else:
             finalResultName = '%s.%s' % (resPrefix, resultType)
         if not os.path.exists(finalResultName):
             raise PodError(CONVERT_ERROR % output)
         os.rename(finalResultName, self.result)
Пример #8
0
         # This is an empty leaf folder. We must create an entry in the
         # zip for him
         folderName = dir[len(self.unzipFolder):]
         zInfo = zipfile.ZipInfo("%s/" % folderName,
                                 time.localtime()[:6])
         zInfo.external_attr = 48
         resultOdt.writestr(zInfo, '')
 resultOdt.close()
 resultType = os.path.splitext(self.result)[1]
 try:
     if (resultType == '.odt') and not self.forceOoCall:
         # Simply move the ODT result to the result
         os.rename(resultOdtName, self.result)
     else:
         if resultType.startswith('.'): resultType = resultType[1:]
         if not resultType in FILE_TYPES.keys():
             raise PodError(BAD_RESULT_TYPE %
                            (self.result, FILE_TYPES.keys()))
         # Call OpenOffice to perform the conversion or document update
         output = self.callOpenOffice(resultOdtName, resultType)
         # I (should) have the result. Move it to the correct name
         resPrefix = os.path.splitext(resultOdtName)[0] + '.'
         if resultType == 'odt':
             # converter.py has (normally!) created a second file
             # suffixed .res.odt
             resultName = resPrefix + 'res.odt'
             if not os.path.exists(resultName):
                 resultName = resultOdtName
                 # In this case OO in server mode could not be called to
                 # update indexes, sections, etc.
         else:
Пример #9
0
class Renderer:
    templateTypes = ('odt', 'ods')  # Types of POD templates

    def __init__(self,
                 template,
                 context,
                 result,
                 pythonWithUnoPath=None,
                 ooPort=2002,
                 stylesMapping={},
                 forceOoCall=False,
                 finalizeFunction=None,
                 overwriteExisting=False,
                 raiseOnError=False,
                 imageResolver=None,
                 stylesTemplate=None):
        '''This Python Open Document Renderer (PodRenderer) loads a document
           template (p_template) which is an ODT or ODS file with some elements
           written in Python. Based on this template and some Python objects
           defined in p_context, the renderer generates an ODT file (p_result)
           that instantiates the p_template and fills it with objects from the
           p_context.

         - If p_result does not end with .odt or .ods, the Renderer will call
           LibreOffice to perform a conversion. If p_forceOoCall is True, even
           if p_result ends with .odt, LibreOffice will be called, not for
           performing a conversion, but for updating some elements like indexes
           (table of contents, etc) and sections containing links to external
           files (which is the case, for example, if you use the default
           function "document").

         - If the Python interpreter which runs the current script is not
           UNO-enabled, this script will run, in another process, a UNO-enabled
           Python interpreter (whose path is p_pythonWithUnoPath) which will
           call LibreOffice. In both cases, we will try to connect to
           LibreOffice in server mode on port p_ooPort.

         - If you plan to make "XHTML to OpenDocument" conversions, you may
           specify a styles mapping in p_stylesMapping.

         - If you specify a function in p_finalizeFunction, this function will
           be called by the renderer before re-zipping the ODT/S result. This
           way, you can still perform some actions on the content of the ODT/S
           file before it is zipped and potentially converted. This function
           must accept one arg: the absolute path to the temporary folder
           containing the un-zipped content of the ODT/S result.

         - If you set p_overwriteExisting to True, the renderer will overwrite
           the result file. Else, an exception will be thrown if the result file
           already exists.

         - If p_raiseOnError is False (the default value), any error encountered
           during the generation of the result file will be dumped into it, as
           a Python traceback within a note. Else, the error will be raised.

         - p_imageResolver allows POD to retrieve images, from "img" tags within
           XHTML content. Indeed, POD may not be able (ie, may not have the
           permission to) perform a HTTP GET on those images. Currently, the
           resolver can only be a Zope application object.

         - p_stylesTemplate can be the path to a LibreOffice file (ie, a .ott
           file) whose styles will be imported within the result.
        '''
        self.template = template
        self.result = result
        self.contentXml = None  # Content (string) of content.xml
        self.stylesXml = None  # Content (string) of styles.xml
        self.stylesManager = None  # Manages the styles defined into the ODT
        # template
        self.tempFolder = None
        self.env = None
        self.pyPath = pythonWithUnoPath
        self.ooPort = ooPort
        self.forceOoCall = forceOoCall
        self.finalizeFunction = finalizeFunction
        self.overwriteExisting = overwriteExisting
        self.raiseOnError = raiseOnError
        self.imageResolver = imageResolver
        self.stylesTemplate = stylesTemplate
        # Remember potential files or images that will be included through
        # "do ... from document" statements: we will need to declare them in
        # META-INF/manifest.xml. Keys are file names as they appear within the
        # ODT file (to dump in manifest.xml); values are original paths of
        # included images (used for avoiding to create multiple copies of a file
        # which is imported several times).
        self.fileNames = {}
        self.prepareFolders()
        # Unzip template
        self.unzipFolder = os.path.join(self.tempFolder, 'unzip')
        os.mkdir(self.unzipFolder)
        info = unzip(template, self.unzipFolder, odf=True)
        self.contentXml = info['content.xml'].decode('utf-8')
        self.stylesXml = info['styles.xml'].decode('utf-8')
        self.stylesManager = StylesManager(self.stylesXml)
        # From LibreOffice 3.5, it is not possible anymore to dump errors into
        # the resulting ods as annotations. Indeed, annotations can't reside
        # anymore within paragraphs. ODS files generated with pod and containing
        # error messages in annotations cause LibreOffice 3.5 and 4.0 to crash.
        # LibreOffice >= 4.1 simply does not show the annotation.
        if info['mimetype'] == mimeTypes['ods']: self.raiseOnError = True
        # Create the content.xml parser
        pe = PodEnvironment
        contentInserts = (OdInsert(CONTENT_POD_FONTS,
                                   XmlElement('font-face-decls',
                                              nsUri=pe.NS_OFFICE),
                                   nsUris={
                                       'style': pe.NS_STYLE,
                                       'svg': pe.NS_SVG
                                   }),
                          OdInsert(CONTENT_POD_STYLES,
                                   XmlElement('automatic-styles',
                                              nsUri=pe.NS_OFFICE),
                                   nsUris={
                                       'style': pe.NS_STYLE,
                                       'fo': pe.NS_FO,
                                       'text': pe.NS_TEXT,
                                       'table': pe.NS_TABLE
                                   }))
        self.contentParser = self.createPodParser('content.xml', context,
                                                  contentInserts)
        # Create the styles.xml parser
        stylesInserts = (OdInsert(STYLES_POD_FONTS,
                                  XmlElement('font-face-decls',
                                             nsUri=pe.NS_OFFICE),
                                  nsUris={
                                      'style': pe.NS_STYLE,
                                      'svg': pe.NS_SVG
                                  }),
                         OdInsert(STYLES_POD_STYLES,
                                  XmlElement('styles', nsUri=pe.NS_OFFICE),
                                  nsUris={
                                      'style': pe.NS_STYLE,
                                      'fo': pe.NS_FO,
                                      'text': pe.NS_TEXT
                                  }))
        self.stylesParser = self.createPodParser('styles.xml', context,
                                                 stylesInserts)
        # Store the styles mapping
        self.setStylesMapping(stylesMapping)
        # While working, POD may identify "dynamic styles" to insert into
        # the "automatic styles" section of content.xml, like the column styles
        # of tables generated from XHTML tables via xhtml2odt.py.
        self.dynamicStyles = []

    def createPodParser(self, odtFile, context, inserts):
        '''Creates the parser with its environment for parsing the given
           p_odtFile (content.xml or styles.xml). p_context is given by the pod
           user, while p_inserts depends on the ODT file we must parse.'''
        evalContext = {
            'xhtml': self.renderXhtml,
            'text': self.renderText,
            'test': self.evalIfExpression,
            'document': self.importDocument,
            'pod': self.importPod,
            'pageBreak': self.insertPageBreak
        }  # Default context
        if hasattr(context, '__dict__'):
            evalContext.update(context.__dict__)
        elif isinstance(context, dict) or isinstance(context, UserDict):
            evalContext.update(context)
        else:
            raise PodError(BAD_CONTEXT)
        env = PodEnvironment(evalContext, inserts)
        fileBuffer = FileBuffer(env, os.path.join(self.tempFolder, odtFile))
        env.currentBuffer = fileBuffer
        return PodParser(env, self)

    def renderXhtml(self, xhtmlString, encoding='utf-8', stylesMapping={}):
        '''Method that can be used (under the name 'xhtml') into a pod template
           for converting a chunk of XHTML content (p_xhtmlString) into a chunk
           of ODT content.'''
        stylesMapping = self.stylesManager.checkStylesMapping(stylesMapping)
        # xhtmlString can only be a chunk of XHTML. So we must surround it with
        # a tag in order to get a XML-compliant file (we need a root tag).
        if xhtmlString == None: xhtmlString = ''
        xhtmlContent = '<p>%s</p>' % xhtmlString
        return Xhtml2OdtConverter(xhtmlContent, encoding, self.stylesManager,
                                  stylesMapping, self).run()

    def renderText(self, text, encoding='utf-8', stylesMapping={}):
        '''Obsolete method.'''
        raise Exception(OBSOLETE_RENDER_TEXT)

    def evalIfExpression(self, condition, ifTrue, ifFalse):
        '''This method implements the method 'test' which is proposed in the
           default pod context. It represents an 'if' expression (as opposed to
           the 'if' statement): depending on p_condition, expression result is
           p_ifTrue or p_ifFalse.'''
        if condition:
            return ifTrue
        return ifFalse

    imageFormats = ('png', 'jpeg', 'jpg', 'gif', 'svg')
    ooFormats = ('odt', )
    convertibleFormats = list(FILE_TYPES.keys())

    def importDocument(self,
                       content=None,
                       at=None,
                       format=None,
                       anchor='as-char',
                       wrapInPara=True,
                       size=None,
                       sizeUnit='cm',
                       style=None,
                       pageBreakBefore=False,
                       pageBreakAfter=False):
        '''If p_at is not None, it represents a path or url allowing to find
           the document. If p_at is None, the content of the document is
           supposed to be in binary format in p_content. The document
           p_format may be: odt or any format in imageFormats.

           p_anchor, p_wrapInPara and p_size, p_sizeUnit and p_style are only
           relevant for images:
           * p_anchor defines the way the image is anchored into the document;
                      Valid values are 'page','paragraph', 'char' and 'as-char';
           * p_wrapInPara, if true, wraps the resulting 'image' tag into a 'p'
                           tag;
           * p_size, if specified, is a tuple of float or integers
                     (width, height) expressing size in p_sizeUnit (see below).
                     If not specified, size will be computed from image info;
           * p_sizeUnit is the unit for p_size elements, it can be "cm"
             (centimeters), "px" (pixels) or "pc" (percentage). Percentages, in
             p_size, must be expressed as integers from 1 to 100.
           * if p_style is given, it is the content of a "style" attribute,
             containing CSS attributes. If "width" and "heigth" attributes are
             found there, they will override p_size and p_sizeUnit.

           p_pageBreakBefore and p_pageBreakAfter are only relevant for import
           of external odt documents, and allows to insert a page break
           before/after the inserted document.
        '''
        importer = None
        # Is there someting to import?
        if not content and not at: raise PodError(DOC_NOT_SPECIFIED)
        # Convert Zope files into Appy wrappers.
        if content.__class__.__name__ in ('File', 'Image'):
            content = FileWrapper(content)
        # Guess document format
        if isinstance(content, FileWrapper):
            format = content.mimeType
        if not format:
            # It should be deduced from p_at
            if not at:
                raise PodError(DOC_FORMAT_ERROR)
            format = os.path.splitext(at)[1][1:]
        else:
            # If format is a mimeType, convert it to an extension
            if format in mimeTypesExts:
                format = mimeTypesExts[format]
        isImage = False
        isOdt = False
        if format in self.ooFormats:
            importer = OdtImporter
            self.forceOoCall = True
            isOdt = True
        elif (format in self.imageFormats) or not format:
            # If the format can't be guessed, we suppose it is an image.
            importer = ImageImporter
            isImage = True
        elif format == 'pdf':
            importer = PdfImporter
        elif format in self.convertibleFormats:
            importer = ConvertImporter
        else:
            raise PodError(DOC_WRONG_FORMAT % format)
        imp = importer(content, at, format, self)
        # Initialise image-specific parameters
        if isImage: imp.init(anchor, wrapInPara, size, sizeUnit, style)
        elif isOdt: imp.init(pageBreakBefore, pageBreakAfter)
        return imp.run()

    def importPod(self,
                  content=None,
                  at=None,
                  format='odt',
                  context=None,
                  pageBreakBefore=False,
                  pageBreakAfter=False):
        '''Similar to m_importDocument, but allows to import the result of
           executing the POD template specified in p_content or p_at, and
           include it in the POD result.'''
        # Is there a pod template defined?
        if not content and not at:
            raise PodError(DOC_NOT_SPECIFIED)
        # If the POD template is specified as a Zope file, convert it into a
        # Appy FileWrapper.
        if content.__class__.__name__ == 'File':
            content = FileWrapper(content)
        imp = PodImporter(content, at, format, self)
        self.forceOoCall = True
        # Define the context to use: either the current context of the current
        # POD renderer, or p_context if given.
        if context:
            ctx = context
        else:
            ctx = self.contentParser.env.context
        imp.init(ctx, pageBreakBefore, pageBreakAfter)
        return imp.run()

    def insertPageBreak(self):
        '''Inserts a page break into the result.'''
        textNs = self.currentParser.env.namespaces[PodEnvironment.NS_TEXT]
        return '<%s:p %s:style-name="podPageBreak"></%s:p>' % \
               (textNs, textNs, textNs)

    def prepareFolders(self):
        # Check if I can write the result
        if not self.overwriteExisting and os.path.exists(self.result):
            raise PodError(RESULT_FILE_EXISTS % self.result)
        try:
            f = open(self.result, 'w')
            f.write('Hello')
            f.close()
        except OSError as oe:
            raise PodError(CANT_WRITE_RESULT % (self.result, oe))
        except IOError as ie:
            raise PodError(CANT_WRITE_RESULT % (self.result, ie))
        self.result = os.path.abspath(self.result)
        os.remove(self.result)
        # Create a temp folder for storing temporary files
        absResult = os.path.abspath(self.result)
        self.tempFolder = '%s.%f' % (absResult, time.time())
        try:
            os.mkdir(self.tempFolder)
        except OSError as oe:
            raise PodError(CANT_WRITE_TEMP_FOLDER % (self.result, oe))

    def patchManifest(self):
        '''Declares, in META-INF/manifest.xml, images or files included via the
           "do... from document" statements if any.'''
        if self.fileNames:
            j = os.path.join
            toInsert = ''
            for fileName in self.fileNames.keys():
                if fileName.endswith('.svg'):
                    fileName = os.path.splitext(fileName)[0] + '.png'
                mimeType = mimetypes.guess_type(fileName)[0]
                toInsert += ' <manifest:file-entry manifest:media-type="%s" ' \
                            'manifest:full-path="%s"/>\n' % (mimeType, fileName)
            manifestName = j(self.unzipFolder, j('META-INF', 'manifest.xml'))
            f = open(manifestName)
            manifestContent = f.read()
            hook = '</manifest:manifest>'
            manifestContent = manifestContent.replace(hook, toInsert + hook)
            f.close()
            # Write the new manifest content
            f = open(manifestName, 'w')
            f.write(manifestContent)
            f.close()

    # Public interface
    def run(self):
        '''Renders the result'''
        try:
            # Remember which parser is running
            self.currentParser = self.contentParser
            # Create the resulting content.xml
            self.currentParser.parse(self.contentXml)
            self.currentParser = self.stylesParser
            # Create the resulting styles.xml
            self.currentParser.parse(self.stylesXml)
            # Patch META-INF/manifest.xml
            self.patchManifest()
            # Re-zip the result
            self.finalize()
        finally:
            FolderDeleter.delete(self.tempFolder)

    def getStyles(self):
        '''Returns a dict of the styles that are defined into the template.'''
        return self.stylesManager.styles

    def setStylesMapping(self, stylesMapping):
        '''Establishes a correspondence between, on one hand, CSS styles or
           XHTML tags that will be found inside XHTML content given to POD,
           and, on the other hand, ODT styles found into the template.'''
        try:
            stylesMapping = self.stylesManager.checkStylesMapping(
                stylesMapping)
            # The predefined styles below are currently ignored, because the
            # xhtml2odt parser does not take into account span tags.
            if 'span[font-weight=bold]' not in stylesMapping:
                stylesMapping['span[font-weight=bold]'] = 'podBold'
            if 'span[font-style=italic]' not in stylesMapping:
                stylesMapping['span[font-style=italic]'] = 'podItalic'
            self.stylesManager.stylesMapping = stylesMapping
        except PodError as po:
            self.contentParser.env.currentBuffer.content.close()
            self.stylesParser.env.currentBuffer.content.close()
            if os.path.exists(self.tempFolder):
                FolderDeleter.delete(self.tempFolder)
            raise po

    def callLibreOffice(self, resultName, resultType):
        '''Call LibreOffice in server mode to convert or update the result.'''
        loOutput = ''
        try:
            if (not isinstance(self.ooPort, int)) and \
               (not isinstance(self.ooPort, int)):
                raise PodError(BAD_OO_PORT % str(self.ooPort))
            try:
                from appy.pod.converter import Converter, ConverterError
                try:
                    Converter(resultName, resultType, self.ooPort,
                              self.stylesTemplate).run()
                except ConverterError as ce:
                    raise PodError(CONVERT_ERROR % str(ce))
            except ImportError:
                # I do not have UNO. So try to launch a UNO-enabled Python
                # interpreter which should be in self.pyPath.
                if not self.pyPath:
                    raise PodError(NO_PY_PATH % resultType)
                if self.pyPath.find(' ') != -1:
                    raise PodError(BLANKS_IN_PATH % self.pyPath)
                if not os.path.isfile(self.pyPath):
                    raise PodError(PY_PATH_NOT_FILE % self.pyPath)
                if resultName.find(' ') != -1:
                    qResultName = '"%s"' % resultName
                else:
                    qResultName = resultName
                convScript = '%s/converter.py' % \
                            os.path.dirname(appy.pod.__file__)
                if convScript.find(' ') != -1:
                    convScript = '"%s"' % convScript
                cmd = '%s %s %s %s -p%d' % \
                    (self.pyPath, convScript, qResultName, resultType,
                    self.ooPort)
                if self.stylesTemplate: cmd += ' -t%s' % self.stylesTemplate
                loOutput = executeCommand(cmd)
        except PodError as pe:
            # When trying to call LO in server mode for producing ODT or ODS
            # (=forceOoCall=True), if an error occurs we have nevertheless
            # an ODT or ODS to return to the user. So we produce a warning
            # instead of raising an error.
            if (resultType in self.templateTypes) and self.forceOoCall:
                print((WARNING_INCOMPLETE_OD % str(pe)))
            else:
                raise pe
        return loOutput

    def getTemplateType(self):
        '''Identifies the type of the pod template in self.template
           (ods or odt). If self.template is a string, it is a file name and we
           simply get its extension. Else, it is a binary file in a StringIO
           instance, and we seek the mime type from the first bytes.'''
        if isinstance(self.template, str):
            res = os.path.splitext(self.template)[1][1:]
        else:
            # A StringIO instance
            self.template.seek(0)
            firstBytes = self.template.read(90)
            firstBytes = firstBytes[firstBytes.index('mimetype') + 8:]
            if firstBytes.startswith(mimeTypes['ods']):
                res = 'ods'
            else:
                # We suppose this is ODT
                res = 'odt'
        return res

    def finalize(self):
        '''Re-zip the result and potentially call LibreOffice if target format
           is not among self.templateTypes or if forceOoCall is True.'''
        for innerFile in ('content.xml', 'styles.xml'):
            shutil.copy(os.path.join(self.tempFolder, innerFile),
                        os.path.join(self.unzipFolder, innerFile))
        # Insert dynamic styles
        contentXml = os.path.join(self.unzipFolder, 'content.xml')
        f = open(contentXml, 'r+', encoding='utf-8')
        dynamicStyles = ''.join(self.dynamicStyles)
        content = f.read().replace('<!DYNAMIC_STYLES!>', dynamicStyles)
        f.seek(0)
        f.truncate(0)
        f.write(content)
        f.close()
        # Call the user-defined "finalize" function when present
        if self.finalizeFunction:
            try:
                self.finalizeFunction(self.unzipFolder)
            except Exception as e:
                print((WARNING_FINALIZE_ERROR % str(e)))
        # Re-zip the result, first as an OpenDocument file of the same type as
        # the POD template (odt, ods...)
        resultExt = self.getTemplateType()
        resultName = os.path.join(self.tempFolder, 'result.%s' % resultExt)
        zip(resultName, self.unzipFolder, odf=True)
        resultType = os.path.splitext(self.result)[1].strip('.')
        if (resultType in self.templateTypes) and not self.forceOoCall:
            # Simply move the ODT result to the result
            os.rename(resultName, self.result)
        else:
            if resultType not in FILE_TYPES:
                raise PodError(BAD_RESULT_TYPE %
                               (self.result, FILE_TYPES.keys()))
            # Call LibreOffice to perform the conversion or document update.
            output = self.callLibreOffice(resultName, resultType)
            # I (should) have the result. Move it to the correct name.
            resPrefix = os.path.splitext(resultName)[0]
            if resultType in self.templateTypes:
                # converter.py has (normally!) created a second file
                # suffixed .res.[resultType]
                finalResultName = '%s.res.%s' % (resPrefix, resultType)
                if not os.path.exists(finalResultName):
                    finalResultName = resultName
                    # In this case OO in server mode could not be called to
                    # update indexes, sections, etc.
            else:
                finalResultName = '%s.%s' % (resPrefix, resultType)
            if not os.path.exists(finalResultName):
                raise PodError(CONVERT_ERROR % output)
            os.rename(finalResultName, self.result)
Пример #10
0
         # This is an empty leaf folder. We must create an entry in the
         # zip for him
         folderName = dir[len(self.unzipFolder):]
         zInfo = zipfile.ZipInfo("%s/" % folderName,
                                 time.localtime()[:6])
         zInfo.external_attr = 48
         resultOdt.writestr(zInfo, '')
 resultOdt.close()
 resultType = os.path.splitext(self.result)[1]
 try:
     if (resultType == '.odt') and not self.forceOoCall:
         # Simply move the ODT result to the result
         os.rename(resultOdtName, self.result)
     else:
         if resultType.startswith('.'): resultType = resultType[1:]
         if not resultType in FILE_TYPES.keys():
             raise PodError(
                 BAD_RESULT_TYPE % (self.result, FILE_TYPES.keys()))
         # Call OpenOffice to perform the conversion or document update
         output = self.callOpenOffice(resultOdtName, resultType)
         # I (should) have the result. Move it to the correct name
         resPrefix = os.path.splitext(resultOdtName)[0] + '.'
         if resultType == 'odt':
             # converter.py has (normally!) created a second file
             # suffixed .res.odt
             resultName = resPrefix + 'res.odt'
             if not os.path.exists(resultName):
                 resultName = resultOdtName
                 # In this case OO in server mode could not be called to
                 # update indexes, sections, etc.
         else:
Пример #11
0
 def finalize(self):
     '''Re-zip the result and potentially call LibreOffice if target format
        is not among self.templateTypes or if forceOoCall is True.'''
     for innerFile in ('content.xml', 'styles.xml'):
         shutil.copy(os.path.join(self.tempFolder, innerFile),
                     os.path.join(self.unzipFolder, innerFile))
     # Insert dynamic styles
     contentXml = os.path.join(self.unzipFolder, 'content.xml')
     f = open(contentXml)
     dynamicStyles = ''.join(self.dynamicStyles)
     content = f.read().replace('<!DYNAMIC_STYLES!>', dynamicStyles)
     f.close()
     f = open(contentXml, 'w')
     f.write(content)
     f.close()
     # Call the user-defined "finalize" function when present.
     if self.finalizeFunction:
         try:
             self.finalizeFunction(self.unzipFolder)
         except (Exception, e):
             print(WARNING_FINALIZE_ERROR % str(e))
     # Re-zip the result, first as an OpenDocument file of the same type as
     # the POD template (odt, ods...)
     resultExt = self.getTemplateType()
     resultName = os.path.join(self.tempFolder, 'result.%s' % resultExt)
     try:
         resultZip = zipfile.ZipFile(resultName, 'w', zipfile.ZIP_DEFLATED)
     except RuntimeError:
         resultZip = zipfile.ZipFile(resultName, 'w')
     # Insert first the file "mimetype" (uncompressed), in order to be
     # compliant with the OpenDocument Format specification, section 17.4,
     # that expresses this restriction. Else, libraries like "magic", under
     # Linux/Unix, are unable to detect the correct mimetype for a pod result
     # (it simply recognizes it as a "application/zip" and not a
     # "application/vnd.oasis.opendocument.text)".
     mimetypeFile = os.path.join(self.unzipFolder, 'mimetype')
     # This file may not exist (presumably, ods files from Google Drive)
     if not os.path.exists(mimetypeFile):
         f = open(mimetypeFile, 'w')
         f.write(mimeTypes[resultExt])
         f.close()
     resultZip.write(mimetypeFile, 'mimetype', zipfile.ZIP_STORED)
     for dir, dirnames, filenames in os.walk(self.unzipFolder):
         for f in filenames:
             folderName = dir[len(self.unzipFolder) + 1:]
             # Ignore file "mimetype" that was already inserted.
             if (folderName == '') and (f == 'mimetype'): continue
             resultZip.write(os.path.join(dir, f),
                             os.path.join(folderName, f))
         if not dirnames and not filenames:
             # This is an empty leaf folder. We must create an entry in the
             # zip for him.
             folderName = dir[len(self.unzipFolder):]
             zInfo = zipfile.ZipInfo("%s/" % folderName,
                                     time.localtime()[:6])
             zInfo.external_attr = 48
             resultZip.writestr(zInfo, '')
     resultZip.close()
     resultType = os.path.splitext(self.result)[1].strip('.')
     if (resultType in self.templateTypes) and not self.forceOoCall:
         # Simply move the ODT result to the result
         os.rename(resultName, self.result)
     else:
         if resultType not in FILE_TYPES:
             raise PodError(BAD_RESULT_TYPE %
                            (self.result, FILE_TYPES.keys()))
         # Call LibreOffice to perform the conversion or document update.
         output = self.callLibreOffice(resultName, resultType)
         # I (should) have the result. Move it to the correct name.
         resPrefix = os.path.splitext(resultName)[0]
         if resultType in self.templateTypes:
             # converter.py has (normally!) created a second file
             # suffixed .res.[resultType]
             finalResultName = '%s.res.%s' % (resPrefix, resultType)
             if not os.path.exists(finalResultName):
                 finalResultName = resultName
                 # In this case OO in server mode could not be called to
                 # update indexes, sections, etc.
         else:
             finalResultName = '%s.%s' % (resPrefix, resultType)
         if not os.path.exists(finalResultName):
             raise PodError(CONVERT_ERROR % output)
         os.rename(finalResultName, self.result)