appypod-rattail/pod/renderer.py

437 lines
21 KiB
Python
Raw Normal View History

2009-06-29 07:06:01 -05:00
# ------------------------------------------------------------------------------
# Appy is a framework for building applications in the Python language.
# Copyright (C) 2007 Gaetan Delannay
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------
import zipfile, shutil, xml.sax, os, os.path, re
from UserDict import UserDict
import appy.pod
from appy.pod import PodError
from appy.shared import mimeTypesExts
2009-06-29 07:06:01 -05:00
from appy.shared.xml_parser import XmlElement
from appy.shared.utils import FolderDeleter, executeCommand
2009-06-29 07:06:01 -05:00
from appy.pod.pod_parser import PodParser, PodEnvironment, OdInsert
from appy.pod.converter import FILE_TYPES
2009-06-29 07:06:01 -05:00
from appy.pod.buffers import FileBuffer
from appy.pod.xhtml2odt import Xhtml2OdtConverter
from appy.pod.doc_importers import OdtImporter, ImageImporter, PdfImporter
from appy.pod.styles_manager import StylesManager
# ------------------------------------------------------------------------------
BAD_CONTEXT = 'Context must be either a dict, a UserDict or an instance.'
RESULT_FILE_EXISTS = 'Result file "%s" exists.'
CANT_WRITE_RESULT = 'I cannot write result file "%s". %s'
TEMP_FOLDER_EXISTS = 'I need to use a temp folder "%s" but this folder ' \
'already exists.'
CANT_WRITE_TEMP_FOLDER = 'I cannot create temp folder "%s". %s'
NO_PY_PATH = 'Extension of result file is "%s". In order to perform ' \
'conversion from ODT to this format we need to call OpenOffice. ' \
'But the Python interpreter which runs the current script does ' \
'not know UNO, the library that allows to connect to ' \
'OpenOffice in server mode. If you can\'t install UNO in this ' \
'Python interpreter, you can specify, in parameter ' \
'"pythonWithUnoPath", the path to a UNO-enabled Python ' \
'interpreter. One such interpreter may be found in ' \
'<open_office_path>/program.'
PY_PATH_NOT_FILE = '"%s" is not a file. You must here specify the absolute ' \
'path of a Python interpreter (.../python, .../python.sh, ' \
'.../python.exe, .../python.bat...).'
BLANKS_IN_PATH = 'Blanks were found in path "%s". Please use the DOS-names ' \
'(ie, "progra~1" instead of "Program files" or "docume~1" ' \
'instead of "Documents and settings".'
BAD_RESULT_TYPE = 'Result "%s" has a wrong extension. Allowed extensions ' \
'are: "%s".'
CONVERT_ERROR = 'An error occurred during the conversion. %s'
BAD_OO_PORT = 'Bad OpenOffice port "%s". Make sure it is an integer.'
XHTML_ERROR = 'An error occurred while rendering XHTML content.'
WARNING_INCOMPLETE_ODT = 'Warning: your ODT file may not be complete (ie ' \
'imported documents may not be present). This is ' \
'because we could not connect to OpenOffice in ' \
'server mode: %s'
DOC_NOT_SPECIFIED = 'Please specify a document to import, either with a ' \
'stream (parameter "content") or with a path (parameter ' \
'"at")'
DOC_FORMAT_ERROR = 'POD was unable to deduce the document format. Please ' \
'specify it through parameter named "format" (=odt, gif, ' \
'png, ...).'
DOC_WRONG_FORMAT = 'Format "%s" is not supported.'
WARNING_FINALIZE_ERROR = 'Warning: error while calling finalize function. %s'
# Default automatic text styles added by pod in content.xml
f = open('%s/styles.in.content.xml' % os.path.dirname(appy.pod.__file__))
CONTENT_POD_STYLES = f.read()
f.close()
# Default font added by pod in content.xml
CONTENT_POD_FONTS = '<@style@:font-face style:name="PodStarSymbol" ' \
'@svg@:font-family="StarSymbol"/>'
# Default text styles added by pod in styles.xml
f = file('%s/styles.in.styles.xml' % os.path.dirname(appy.pod.__file__))
STYLES_POD_STYLES = f.read()
f.close()
# Default font added by pod
STYLES_POD_FONTS = '<@style@:font-face @style@:name="PodStarSymbol" ' \
'@svg@:font-family="StarSymbol"/>'
# ------------------------------------------------------------------------------
class Renderer:
def __init__(self, template, context, result, pythonWithUnoPath=None,
ooPort=2002, stylesMapping={}, forceOoCall=False,
finalizeFunction=None):
'''This Python Open Document Renderer (PodRenderer) loads a document
template (p_template) which is an ODT 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, the Renderer
will call OpenOffice to perform a conversion. If p_forceOoCall is
True, even if p_result ends with .odt, OpenOffice 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 OpenOffice. In both cases, we will try to connect to OpenOffice
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 result. This way,
you can still perform some actions on the content of the ODT 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 result.'''
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.curdir = os.getcwd()
self.env = None
self.pyPath = pythonWithUnoPath
self.ooPort = ooPort
self.forceOoCall = forceOoCall
self.finalizeFunction = finalizeFunction
self.prepareFolders()
# Unzip template
self.unzipFolder = os.path.join(self.tempFolder, 'unzip')
os.mkdir(self.unzipFolder)
for zippedFile in self.templateZip.namelist():
fileName = os.path.basename(zippedFile)
folderName = os.path.dirname(zippedFile)
# Create folder if needed
fullFolderName = self.unzipFolder
if folderName:
fullFolderName = os.path.join(fullFolderName, folderName)
if not os.path.exists(fullFolderName):
os.makedirs(fullFolderName)
# Unzip file
if fileName:
fullFileName = os.path.join(fullFolderName, fileName)
f = open(fullFileName, 'wb')
fileContent = self.templateZip.read(zippedFile)
if fileName == 'content.xml':
self.contentXml = fileContent
elif fileName == 'styles.xml':
self.stylesManager = StylesManager(fileContent)
self.stylesXml = fileContent
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}))
self.stylesParser = self.createPodParser('styles.xml', context,
stylesInserts)
# Stores the styles mapping
self.setStylesMapping(stylesMapping)
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,
'test': self.evalIfExpression,
'document': self.importDocument} # 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)
ns = self.currentParser.env.namespaces
# xhtmlString is only a chunk of XHTML. So we must surround it a tag in
# order to get a XML-compliant file (we need a root tag)
xhtmlContent = '<podXhtml>%s</podXhtml>' % xhtmlString
return Xhtml2OdtConverter(xhtmlContent, encoding, self.stylesManager,
stylesMapping, ns).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
imageFormats = ('png', 'jpeg', 'jpg', 'gif')
ooFormats = ('odt',)
def importDocument(self, content=None, at=None, format=None,
anchor='as-char'):
'''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 is only
relevant for images.'''
ns = self.currentParser.env.namespaces
importer = None
# Is there someting to import?
if not content and not at:
raise PodError(DOC_NOT_SPECIFIED)
# Guess document format
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]
2009-06-29 07:06:01 -05:00
isImage = False
if format in self.ooFormats:
importer = OdtImporter
self.forceOoCall = True
elif format in self.imageFormats:
importer = ImageImporter
isImage = True
elif format == 'pdf':
importer = PdfImporter
else:
raise PodError(DOC_WRONG_FORMAT % format)
imp = importer(content, at, format, self.tempFolder, ns)
if isImage:
imp.setAnchor(anchor)
return imp.run()
def prepareFolders(self):
# Check if I can write the result
if 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))
2009-06-29 07:06:01 -05:00
self.result = os.path.abspath(self.result)
os.remove(self.result)
# Check that temp folder does not exist
self.tempFolder = os.path.abspath(self.result) + '.temp'
if os.path.exists(self.tempFolder):
raise PodError(TEMP_FOLDER_EXISTS % self.tempFolder)
try:
os.mkdir(self.tempFolder)
except OSError, oe:
raise PodError(CANT_WRITE_TEMP_FOLDER % (self.result, oe))
# Public interface
def run(self):
'''Renders the result.'''
# 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)
# Re-zip the result
self.finalize()
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 correspondance 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)
self.stylesManager.stylesMapping = stylesMapping
except PodError, po:
self.contentParser.env.currentBuffer.content.close()
self.stylesParser.env.currentBuffer.content.close()
2009-06-29 07:06:01 -05:00
if os.path.exists(self.tempFolder):
FolderDeleter.delete(self.tempFolder)
raise po
def reportProblem(self, msg, resultType):
'''When trying to call OO in server mode for producing ODT
(=forceOoCall=True), if an error occurs we still have an ODT to
return to the user. So we produce a warning instead of raising an
error.'''
if (resultType == 'odt') and self.forceOoCall:
print WARNING_INCOMPLETE_ODT % msg
else:
raise msg
def callOpenOffice(self, resultOdtName, resultType):
'''Call Open Office in server mode to convert or update the ODT
result.'''
ooOutput = ''
2009-06-29 07:06:01 -05:00
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(resultOdtName, resultType,
self.ooPort).run()
except ConverterError, 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 resultOdtName.find(' ') != -1:
qResultOdtName = '"%s"' % resultOdtName
else:
qResultOdtName = resultOdtName
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, qResultOdtName, resultType,
self.ooPort)
ooOutput = executeCommand(cmd)
2009-06-29 07:06:01 -05:00
except PodError, pe:
# When trying to call OO in server mode for producing
# ODT (=forceOoCall=True), if an error occurs we still
# have an ODT to return to the user. So we produce a
# warning instead of raising an error.
if (resultType == 'odt') and self.forceOoCall:
print WARNING_INCOMPLETE_ODT % str(pe)
else:
raise pe
return ooOutput
2009-06-29 07:06:01 -05:00
def finalize(self):
'''Re-zip the result and potentially call OpenOffice if target format is
not ODT or if forceOoCall is True.'''
for odtFile in ('content.xml', 'styles.xml'):
shutil.copy(os.path.join(self.tempFolder, odtFile),
os.path.join(self.unzipFolder, odtFile))
if self.finalizeFunction:
try:
self.finalizeFunction(self.unzipFolder)
except Exception, e:
print WARNING_FINALIZE_ERROR % str(e)
resultOdtName = os.path.join(self.tempFolder, 'result.odt')
try:
resultOdt = zipfile.ZipFile(resultOdtName,'w', zipfile.ZIP_DEFLATED)
except RuntimeError:
resultOdt = zipfile.ZipFile(resultOdtName,'w')
2009-06-29 07:06:01 -05:00
os.chdir(self.unzipFolder)
for dir, dirnames, filenames in os.walk('.'):
for f in filenames:
resultOdt.write(os.path.join(dir, f)[2:])
# [2:] is there to avoid havin './' in the path in the zip file.
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():
2009-06-29 07:06:01 -05:00
raise PodError(BAD_RESULT_TYPE % (
self.result, FILE_TYPES.keys()))
2009-06-29 07:06:01 -05:00
# 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
2009-06-29 07:06:01 -05:00
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:
resultName = resPrefix + resultType
if not os.path.exists(resultName):
raise PodError(CONVERT_ERROR % output)
2009-06-29 07:06:01 -05:00
os.rename(resultName, self.result)
finally:
os.chdir(self.curdir)
FolderDeleter.delete(self.tempFolder)
# ------------------------------------------------------------------------------