[gen] [pod] converter.py: added param '-t' allowing to specify a LibreOffice file whose styles will be imported in the pod result (works only for odt files). Thanks to IMIO. [pod] Pod expressions can now be defined in fields of type 'text-input' (in addition to existing fields 'conditional-text' and track-changed text). Thanks to IMIO. [gen] Added parameter 'stylesTemplate' to the Renderer, allowing to specify a LibreOffice file whose styles will be imported in the pod result. Thanks to IMIO. [bin] Added script odfwalk.py allowing to modify or consult the content of odf files in a folder hierarchy (the script manages the unzip and re-zip of odf files and let a caller script access the unzipped content). [pod] Take into account tag 's'. Thanks to IMIO.

This commit is contained in:
Gaetan Delannay 2015-03-13 08:59:32 +01:00
parent 727eec8a91
commit 8168306b57
8 changed files with 292 additions and 167 deletions

75
bin/odfwalk.py Normal file
View file

@ -0,0 +1,75 @@
'''This script allows to walk (and potentially patch) files (content.xml,
styles.xml...) contained within a given ODF file or within all ODF files
found in some folder.'''
# ------------------------------------------------------------------------------
import sys, os.path, time
from appy.shared.zip import unzip, zip
from appy.shared.utils import getOsTempFolder, FolderDeleter, executeCommand
# ------------------------------------------------------------------------------
usage = '''Usage: python odfWalk.py [file|folder] yourScript.
If *file* is given, it is the path to an ODF file (odt or ods). This single
file will be walked.
If *folder* is given, we will walk all ODF files found in this folder and
sub-folders.
*yourScript* is the path to a Python script that will be run on every walked
file. It will be called with a single arg containing the absolute path to the
folder containing the unzipped file content (content.xml, styles.xml...).'''
# ------------------------------------------------------------------------------
class OdfWalk:
toUnzip = ('.ods', '.odt')
def __init__(self, fileOrFolder, script):
self.fileOrFolder = fileOrFolder
self.script = script
self.tempFolder = getOsTempFolder()
def walkFile(self, fileName):
'''Unzip p_fileName in a temp folder, call self.script, and then re-zip
the result.'''
print 'Walking %s...' % fileName
# Create a temp folder
name = 'f%f' % time.time()
tempFolder = os.path.join(self.tempFolder, name)
os.mkdir(tempFolder)
# Unzip the file in it
unzip(fileName, tempFolder)
# Call self.script
py = sys.executable or 'python'
cmd = '%s %s %s' % (py, self.script, tempFolder)
print ' Running %s...' % cmd,
os.system(cmd)
# Re-zip the result
zip(fileName, tempFolder, odf=True)
FolderDeleter.delete(tempFolder)
print 'done.'
def run(self):
if os.path.isfile(self.fileOrFolder):
self.walkFile(self.fileOrFolder)
elif os.path.isdir(self.fileOrFolder):
# Walk all files found in this folder
for dir, dirnames, filenames in os.walk(self.fileOrFolder):
for name in filenames:
if os.path.splitext(name)[1] in self.toUnzip:
self.walkFile(os.path.join(dir, name))
else:
print('%s does not exist.' % self.fileOrFolder)
# ------------------------------------------------------------------------------
if __name__ == '__main__':
if len(sys.argv) != 3:
print(usage)
sys.exit()
# Warn the user.
print 'All the files in %s will be modified. ' \
'Are you sure? [y/N] ' % sys.argv[1],
response = sys.stdin.readline().strip().lower()
if response == 'y':
OdfWalk(sys.argv[1], sys.argv[2]).run()
else:
print 'Canceled.'
# ------------------------------------------------------------------------------

View file

@ -53,7 +53,7 @@ FILE_TYPES = {'odt': 'writer8',
class ConverterError(Exception): pass class ConverterError(Exception): pass
# ConverterError-related messages ---------------------------------------------- # ConverterError-related messages ----------------------------------------------
DOC_NOT_FOUND = 'Document "%s" was not found.' DOC_NOT_FOUND = '"%s" not found.'
URL_NOT_FOUND = 'Doc URL "%s" is wrong. %s' URL_NOT_FOUND = 'Doc URL "%s" is wrong. %s'
BAD_RESULT_TYPE = 'Bad result type "%s". Available types are %s.' BAD_RESULT_TYPE = 'Bad result type "%s". Available types are %s.'
CANNOT_WRITE_RESULT = 'I cannot write result "%s". %s' CANNOT_WRITE_RESULT = 'I cannot write result "%s". %s'
@ -71,9 +71,11 @@ class Converter:
'openoffice.org 1': 'openof~1', 'openoffice.org 1': 'openof~1',
'openoffice.org 2': 'openof~1', 'openoffice.org 2': 'openof~1',
} }
def __init__(self, docPath, resultType, port=DEFAULT_PORT): def __init__(self, docPath, resultType, port=DEFAULT_PORT,
templatePath=None):
self.port = port self.port = port
self.docUrl, self.docPath = self.getInputUrls(docPath) # The path to the document to convert
self.docUrl, self.docPath = self.getFilePath(docPath)
self.inputType = os.path.splitext(docPath)[1][1:].lower() self.inputType = os.path.splitext(docPath)[1][1:].lower()
self.resultType = resultType self.resultType = resultType
self.resultFilter = self.getResultFilter() self.resultFilter = self.getResultFilter()
@ -81,16 +83,21 @@ class Converter:
self.loContext = None self.loContext = None
self.oo = None # The LibreOffice application object self.oo = None # The LibreOffice application object
self.doc = None # The LibreOffice loaded document self.doc = None # The LibreOffice loaded document
# The path to a LibreOffice template (ie, a ".ott" file) from which
# styles can be imported
self.templateUrl = self.templatePath = None
if templatePath:
self.templateUrl, self.templatePath = self.getFilePath(templatePath)
def getInputUrls(self, docPath): def getFilePath(self, filePath):
'''Returns the absolute path of the input file. In fact, it returns a '''Returns the absolute path of p_filePath. In fact, it returns a
tuple with some URL version of the path for OO as the first element tuple with some URL version of the path for LO as the first element
and the absolute path as the second element.''' and the absolute path as the second element.'''
import unohelper import unohelper
if not os.path.exists(docPath) and not os.path.isfile(docPath): if not os.path.exists(filePath) and not os.path.isfile(filePath):
raise ConverterError(DOC_NOT_FOUND % docPath) raise ConverterError(DOC_NOT_FOUND % filePath)
docAbsPath = os.path.abspath(docPath) docAbsPath = os.path.abspath(filePath)
# Return one path for OO, one path for me. # Return one path for OO, one path for me
return unohelper.systemPathToFileUrl(docAbsPath), docAbsPath return unohelper.systemPathToFileUrl(docAbsPath), docAbsPath
def getResultFilter(self): def getResultFilter(self):
@ -132,6 +139,18 @@ class Converter:
e = sys.exc_info()[1] e = sys.exc_info()[1]
raise ConverterError(CANNOT_WRITE_RESULT % (res, e)) raise ConverterError(CANNOT_WRITE_RESULT % (res, e))
def props(self, properties):
'''Create a UNO-compliant tuple of properties, from tuple p_properties
containing sub-tuples (s_propertyName, value).'''
from com.sun.star.beans import PropertyValue
res = []
for name, value in properties:
prop = PropertyValue()
prop.Name = name
prop.Value = value
res.append(prop)
return tuple(res)
def connect(self): def connect(self):
'''Connects to LibreOffice''' '''Connects to LibreOffice'''
if os.name == 'nt': if os.name == 'nt':
@ -161,10 +180,11 @@ class Converter:
raise ConverterError(CONNECT_ERROR % (self.port, e)) raise ConverterError(CONNECT_ERROR % (self.port, e))
def updateOdtDocument(self): def updateOdtDocument(self):
'''If the input file is an ODT document, we will perform 2 tasks: '''If the input file is an ODT document, we will perform those tasks:
1) Update all annexes; 1) update all annexes;
2) Update sections (if sections refer to external content, we try to 2) update sections (if sections refer to external content, we try to
include the content within the result file) include the content within the result file);
3) load styles from an external template if given.
''' '''
from com.sun.star.lang import IndexOutOfBoundsException from com.sun.star.lang import IndexOutOfBoundsException
# I need to use IndexOutOfBoundsException because sometimes, when # I need to use IndexOutOfBoundsException because sometimes, when
@ -197,29 +217,26 @@ class Converter:
# of the section. Else, it won't appear. # of the section. Else, it won't appear.
except IndexOutOfBoundsException: except IndexOutOfBoundsException:
pass pass
# Import styles from an external file when required
if self.templateUrl:
params = self.props(('OverwriteStyles', True),
('LoadPageStyles', False))
self.doc.StyleFamilies.loadStylesFromURL(self.templateUrl, params)
def loadDocument(self): def loadDocument(self):
from com.sun.star.lang import IllegalArgumentException, \ from com.sun.star.lang import IllegalArgumentException, \
IndexOutOfBoundsException IndexOutOfBoundsException
from com.sun.star.beans import PropertyValue
try: try:
# Loads the document to convert in a new hidden frame # Loads the document to convert in a new hidden frame
prop = PropertyValue(); prop.Name = 'Hidden'; prop.Value = True props = [('Hidden', True)]
if self.inputType == 'csv': if self.inputType == 'csv':
# Give some additional params if we need to open a CSV file # Give some additional params if we need to open a CSV file
prop2 = PropertyValue() props.append(('FilterFlags', '59,34,76,1'))
prop2.Name = 'FilterFlags' #props.append(('FilterData', 'Any'))
prop2.Value = '59,34,76,1'
#prop2.Name = 'FilterData'
#prop2.Value = 'Any'
props = (prop, prop2)
else:
props = (prop,)
self.doc = self.oo.loadComponentFromURL(self.docUrl, "_blank", 0, self.doc = self.oo.loadComponentFromURL(self.docUrl, "_blank", 0,
props) self.props(props))
if self.inputType == 'odt':
# Perform additional tasks for odt documents # Perform additional tasks for odt documents
self.updateOdtDocument() if self.inputType == 'odt': self.updateOdtDocument()
try: try:
self.doc.refresh() self.doc.refresh()
except AttributeError: except AttributeError:
@ -232,22 +249,13 @@ class Converter:
'''Calls LO to perform a document conversion. Note that the conversion '''Calls LO to perform a document conversion. Note that the conversion
is not really done if the source and target documents have the same is not really done if the source and target documents have the same
type.''' type.'''
properties = [] props = [('FilterName', self.resultFilter)]
from com.sun.star.beans import PropertyValue if self.resultType == 'csv': # Add options for CSV export (separator...)
prop = PropertyValue() props.append(('FilterOptions', '59,34,76,1'))
prop.Name = 'FilterName' self.doc.storeToURL(self.resultUrl, self.props(props))
prop.Value = self.resultFilter
properties.append(prop)
if self.resultType == 'csv':
# For CSV export, add options (separator, etc)
optionsProp = PropertyValue()
optionsProp.Name = 'FilterOptions'
optionsProp.Value = '59,34,76,1'
properties.append(optionsProp)
self.doc.storeToURL(self.resultUrl, tuple(properties))
def run(self): def run(self):
'''Connects to LO, does the job and disconnects.''' '''Connects to LO, does the job and disconnects'''
self.connect() self.connect()
self.loadDocument() self.loadDocument()
self.convertDocument() self.convertDocument()
@ -274,13 +282,17 @@ class ConverterScript:
help="The port on which LibreOffice runs " \ help="The port on which LibreOffice runs " \
"Default is %d." % DEFAULT_PORT, "Default is %d." % DEFAULT_PORT,
default=DEFAULT_PORT, metavar="PORT", type='int') default=DEFAULT_PORT, metavar="PORT", type='int')
optParser.add_option("-t", "--template", dest="template",
default=None, metavar="TEMPLATE", type='string',
help="The path to a LibreOffice template from " \
"which you may import styles.")
(options, args) = optParser.parse_args() (options, args) = optParser.parse_args()
if len(args) != 2: if len(args) != 2:
sys.stderr.write(WRONG_NB_OF_ARGS) sys.stderr.write(WRONG_NB_OF_ARGS)
sys.stderr.write('\n') sys.stderr.write('\n')
optParser.print_help() optParser.print_help()
sys.exit(ERROR_CODE) sys.exit(ERROR_CODE)
converter = Converter(args[0], args[1], options.port) converter = Converter(args[0], args[1], options.port, options.template)
try: try:
converter.run() converter.run()
except ConverterError: except ConverterError:

View file

@ -83,9 +83,11 @@ class PodEnvironment(OdfEnvironment):
# Current state # Current state
self.state = self.READING_CONTENT self.state = self.READING_CONTENT
# Elements we must ignore (they will not be included in the result) # Elements we must ignore (they will not be included in the result)
self.ignorableElements = None # Will be set after namespace propagation self.ignorableElems = None # Will be set after namespace propagation
# Elements that may be impacted by POD statements # Elements that may be impacted by POD statements
self.impactableElements = None # Idem self.impactableElems = None # Idem
# Elements representing start and end tags surrounding expressions
self.exprStartElems = self.exprEndElems = None # Idem
# Stack of currently visited tables # Stack of currently visited tables
self.tableStack = [] self.tableStack = []
self.tableIndex = -1 self.tableIndex = -1
@ -193,30 +195,36 @@ class PodEnvironment(OdfEnvironment):
# Create a table of names of used tags and attributes (precomputed, # Create a table of names of used tags and attributes (precomputed,
# including namespace, for performance). # including namespace, for performance).
table = ns[self.NS_TABLE] table = ns[self.NS_TABLE]
self.tags = { text = ns[self.NS_TEXT]
'tracked-changes': '%s:tracked-changes' % ns[self.NS_TEXT], office = ns[self.NS_OFFICE]
'change': '%s:change' % ns[self.NS_TEXT], tags = {
'annotation': '%s:annotation' % ns[self.NS_OFFICE], 'tracked-changes': '%s:tracked-changes' % text,
'change-start': '%s:change-start' % ns[self.NS_TEXT], 'change': '%s:change' % text,
'change-end': '%s:change-end' % ns[self.NS_TEXT], 'annotation': '%s:annotation' % office,
'conditional-text': '%s:conditional-text' % ns[self.NS_TEXT], 'change-start': '%s:change-start' % text,
'change-end': '%s:change-end' % text,
'conditional-text': '%s:conditional-text' % text,
'text-input': '%s:text-input' % text,
'table': '%s:table' % table, 'table': '%s:table' % table,
'table-name': '%s:name' % table, 'table-name': '%s:name' % table,
'table-cell': '%s:table-cell' % table, 'table-cell': '%s:table-cell' % table,
'table-column': '%s:table-column' % table, 'table-column': '%s:table-column' % table,
'formula': '%s:formula' % table, 'formula': '%s:formula' % table,
'value-type': '%s:value-type' % ns[self.NS_OFFICE], 'value-type': '%s:value-type' % office,
'value': '%s:value' % ns[self.NS_OFFICE], 'value': '%s:value' % office,
'string-value': '%s:string-value' % ns[self.NS_OFFICE], 'string-value': '%s:string-value' % office,
'span': '%s:span' % ns[self.NS_TEXT], 'span': '%s:span' % text,
'number-columns-spanned': '%s:number-columns-spanned' % table, 'number-columns-spanned': '%s:number-columns-spanned' % table,
'number-columns-repeated': '%s:number-columns-repeated' % table, 'number-columns-repeated': '%s:number-columns-repeated' % table,
} }
self.ignorableElements = (self.tags['tracked-changes'], self.tags = tags
self.tags['change']) self.ignorableElems = (tags['tracked-changes'], tags['change'])
self.impactableElements = ( self.exprStartElems = (tags['change-start'], tags['conditional-text'], \
Text.OD.elem, Title.OD.elem, Table.OD.elem, Row.OD.elem, tags['text-input'])
Cell.OD.elem, Section.OD.elem) self.exprEndElems = (tags['change-end'], tags['conditional-text'], \
tags['text-input'])
self.impactableElems = (Text.OD.elem, Title.OD.elem, Table.OD.elem,
Row.OD.elem, Cell.OD.elem, Section.OD.elem)
self.inserts = self.transformInserts() self.inserts = self.transformInserts()
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -234,15 +242,15 @@ class PodParser(OdfParser):
officeNs = ns[e.NS_OFFICE] officeNs = ns[e.NS_OFFICE]
textNs = ns[e.NS_TEXT] textNs = ns[e.NS_TEXT]
tableNs = ns[e.NS_TABLE] tableNs = ns[e.NS_TABLE]
if elem in e.ignorableElements: if elem in e.ignorableElems:
e.state = e.IGNORING e.state = e.IGNORING
elif elem == e.tags['annotation']: elif elem == e.tags['annotation']:
# Be it in an ODT or ODS template, an annotation is considered to # Be it in an ODT or ODS template, an annotation is considered to
# contain a POD statement. # contain a POD statement.
e.state = e.READING_STATEMENT e.state = e.READING_STATEMENT
elif elem in (e.tags['change-start'], e.tags['conditional-text']): elif elem in e.exprStartElems:
# In an ODT template, any text in track-changes or any conditional # Any track-changed text or being in a conditional or input field is
# field is considered to contain a POD expression. # considered to be a POD expression.
e.state = e.READING_EXPRESSION e.state = e.READING_EXPRESSION
e.exprHasStyle = False e.exprHasStyle = False
elif (elem == e.tags['table-cell']) and \ elif (elem == e.tags['table-cell']) and \
@ -272,7 +280,7 @@ class PodParser(OdfParser):
if e.state == e.IGNORING: if e.state == e.IGNORING:
pass pass
elif e.state == e.READING_CONTENT: elif e.state == e.READING_CONTENT:
if elem in e.impactableElements: if elem in e.impactableElems:
if e.mode == e.ADD_IN_SUBBUFFER: if e.mode == e.ADD_IN_SUBBUFFER:
e.addSubBuffer() e.addSubBuffer()
e.currentBuffer.addElement(e.currentElem.name) e.currentBuffer.addElement(e.currentElem.name)
@ -290,7 +298,7 @@ class PodParser(OdfParser):
ns = e.onEndElement() ns = e.onEndElement()
officeNs = ns[e.NS_OFFICE] officeNs = ns[e.NS_OFFICE]
textNs = ns[e.NS_TEXT] textNs = ns[e.NS_TEXT]
if elem in e.ignorableElements: if elem in e.ignorableElems:
e.state = e.READING_CONTENT e.state = e.READING_CONTENT
elif elem == e.tags['annotation']: elif elem == e.tags['annotation']:
# Manage statement # Manage statement
@ -317,7 +325,7 @@ class PodParser(OdfParser):
e.currentOdsHook = None e.currentOdsHook = None
# Dump the ending tag # Dump the ending tag
e.currentBuffer.dumpEndElement(elem) e.currentBuffer.dumpEndElement(elem)
if elem in e.impactableElements: if elem in e.impactableElems:
if isinstance(e.currentBuffer, MemoryBuffer): if isinstance(e.currentBuffer, MemoryBuffer):
isMainElement = e.currentBuffer.isMainElement(elem) isMainElement = e.currentBuffer.isMainElement(elem)
# Unreference the element among buffer.elements # Unreference the element among buffer.elements
@ -346,8 +354,7 @@ class PodParser(OdfParser):
e.currentStatement.append(statementLine) e.currentStatement.append(statementLine)
e.currentContent = '' e.currentContent = ''
elif e.state == e.READING_EXPRESSION: elif e.state == e.READING_EXPRESSION:
if (elem == e.tags['change-end']) or \ if elem in e.exprEndElems:
(elem == e.tags['conditional-text']):
expression = e.currentContent.strip() expression = e.currentContent.strip()
e.currentContent = '' e.currentContent = ''
# Manage expression # Manage expression

View file

@ -18,13 +18,12 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import zipfile, shutil, xml.sax, os, os.path, re, mimetypes, time import zipfile, shutil, xml.sax, os, os.path, re, mimetypes, time
from UserDict import UserDict from UserDict import UserDict
import appy.pod
import appy.pod, time, cgi
from appy.pod import PodError from appy.pod import PodError
from appy.shared import mimeTypes, mimeTypesExts from appy.shared import mimeTypes, mimeTypesExts
from appy.shared.xml_parser import XmlElement from appy.shared.xml_parser import XmlElement
from appy.shared.zip import unzip, zip
from appy.shared.utils import FolderDeleter, executeCommand, FileWrapper from appy.shared.utils import FolderDeleter, executeCommand, FileWrapper
from appy.pod.pod_parser import PodParser, PodEnvironment, OdInsert from appy.pod.pod_parser import PodParser, PodEnvironment, OdInsert
from appy.pod.converter import FILE_TYPES from appy.pod.converter import FILE_TYPES
@ -101,7 +100,7 @@ class Renderer:
def __init__(self, template, context, result, pythonWithUnoPath=None, def __init__(self, template, context, result, pythonWithUnoPath=None,
ooPort=2002, stylesMapping={}, forceOoCall=False, ooPort=2002, stylesMapping={}, forceOoCall=False,
finalizeFunction=None, overwriteExisting=False, finalizeFunction=None, overwriteExisting=False,
raiseOnError=False, imageResolver=None): raiseOnError=False, imageResolver=None, stylesTemplate=None):
'''This Python Open Document Renderer (PodRenderer) loads a document '''This Python Open Document Renderer (PodRenderer) loads a document
template (p_template) which is an ODT or ODS file with some elements template (p_template) which is an ODT or ODS file with some elements
written in Python. Based on this template and some Python objects written in Python. Based on this template and some Python objects
@ -145,9 +144,11 @@ class Renderer:
XHTML content. Indeed, POD may not be able (ie, may not have the XHTML content. Indeed, POD may not be able (ie, may not have the
permission to) perform a HTTP GET on those images. Currently, the permission to) perform a HTTP GET on those images. Currently, the
resolver can only be a Zope application object. 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.template = template
self.templateZip = zipfile.ZipFile(template)
self.result = result self.result = result
self.contentXml = None # Content (string) of content.xml self.contentXml = None # Content (string) of content.xml
self.stylesXml = None # Content (string) of styles.xml self.stylesXml = None # Content (string) of styles.xml
@ -162,6 +163,7 @@ class Renderer:
self.overwriteExisting = overwriteExisting self.overwriteExisting = overwriteExisting
self.raiseOnError = raiseOnError self.raiseOnError = raiseOnError
self.imageResolver = imageResolver self.imageResolver = imageResolver
self.stylesTemplate = stylesTemplate
# Remember potential files or images that will be included through # Remember potential files or images that will be included through
# "do ... from document" statements: we will need to declare them in # "do ... from document" statements: we will need to declare them in
# META-INF/manifest.xml. Keys are file names as they appear within the # META-INF/manifest.xml. Keys are file names as they appear within the
@ -173,49 +175,16 @@ class Renderer:
# Unzip template # Unzip template
self.unzipFolder = os.path.join(self.tempFolder, 'unzip') self.unzipFolder = os.path.join(self.tempFolder, 'unzip')
os.mkdir(self.unzipFolder) os.mkdir(self.unzipFolder)
for zippedFile in self.templateZip.namelist(): info = unzip(template, self.unzipFolder, odf=True)
# Before writing the zippedFile into self.unzipFolder, create the self.contentXml = info['content.xml']
# intermediary subfolder(s) if needed. self.stylesXml = info['styles.xml']
fileName = None self.stylesManager = StylesManager(self.stylesXml)
if zippedFile.endswith('/') or zippedFile.endswith(os.sep): # From LibreOffice 3.5, it is not possible anymore to dump errors into
# This is an empty folder. Create it nevertheless. If zippedFile # the resulting ods as annotations. Indeed, annotations can't reside
# starts with a '/', os.path.join will consider it an absolute # anymore within paragraphs. ODS files generated with pod and containing
# path and will throw away self.unzipFolder. # error messages in annotations cause LibreOffice 3.5 and 4.0 to crash.
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. # LibreOffice >= 4.1 simply does not show the annotation.
self.raiseOnError = True if info['mimetype'] == mimeTypes['ods']: self.raiseOnError = True
f.write(fileContent)
f.close()
self.templateZip.close()
# Create the content.xml parser # Create the content.xml parser
pe = PodEnvironment pe = PodEnvironment
contentInserts = ( contentInserts = (
@ -440,7 +409,7 @@ class Renderer:
# Public interface # Public interface
def run(self): def run(self):
'''Renders the result.''' '''Renders the result'''
try: try:
# Remember which parser is running # Remember which parser is running
self.currentParser = self.contentParser self.currentParser = self.contentParser
@ -490,7 +459,8 @@ class Renderer:
try: try:
from appy.pod.converter import Converter, ConverterError from appy.pod.converter import Converter, ConverterError
try: try:
Converter(resultName, resultType, self.ooPort).run() Converter(resultName, resultType, self.ooPort,
self.stylesTemplate).run()
except ConverterError, ce: except ConverterError, ce:
raise PodError(CONVERT_ERROR % str(ce)) raise PodError(CONVERT_ERROR % str(ce))
except ImportError: except ImportError:
@ -513,6 +483,7 @@ class Renderer:
cmd = '%s %s %s %s -p%d' % \ cmd = '%s %s %s %s -p%d' % \
(self.pyPath, convScript, qResultName, resultType, (self.pyPath, convScript, qResultName, resultType,
self.ooPort) self.ooPort)
if self.stylesTemplate: cmd += ' -t%s' % self.stylesTemplate
loOutput = executeCommand(cmd) loOutput = executeCommand(cmd)
except PodError, pe: except PodError, pe:
# When trying to call LO in server mode for producing ODT or ODS # When trying to call LO in server mode for producing ODT or ODS
@ -559,7 +530,7 @@ class Renderer:
f = file(contentXml, 'w') f = file(contentXml, 'w')
f.write(content) f.write(content)
f.close() f.close()
# Call the user-defined "finalize" function when present. # Call the user-defined "finalize" function when present
if self.finalizeFunction: if self.finalizeFunction:
try: try:
self.finalizeFunction(self.unzipFolder) self.finalizeFunction(self.unzipFolder)
@ -569,38 +540,7 @@ class Renderer:
# the POD template (odt, ods...) # the POD template (odt, ods...)
resultExt = self.getTemplateType() resultExt = self.getTemplateType()
resultName = os.path.join(self.tempFolder, 'result.%s' % resultExt) resultName = os.path.join(self.tempFolder, 'result.%s' % resultExt)
try: zip(resultName, self.unzipFolder, odf=True)
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('.') resultType = os.path.splitext(self.result)[1].strip('.')
if (resultType in self.templateTypes) and not self.forceOoCall: if (resultType in self.templateTypes) and not self.forceOoCall:
# Simply move the ODT result to the result # Simply move the ODT result to the result

View file

@ -4,18 +4,15 @@
@style@:font-name-asian="PodStarSymbol" @style@:font-size-asian="9pt" @style@:font-name-asian="PodStarSymbol" @style@:font-size-asian="9pt"
@style@:font-name-complex="PodStarSymbol" @style@:font-size-complex="9pt"/> @style@:font-name-complex="PodStarSymbol" @style@:font-size-complex="9pt"/>
</@style@:style> </@style@:style>
<@style@:style style:name="AppyStandard" style:family="paragraph" style:class="text" style:master-page-name=""> <@style@:style style:name="AppyStandard" style:family="paragraph" style:class="text" style:master-page-name="" @style@:parent-style-name="Standard">
<@style@:paragraph-properties fo:margin-left="0cm" fo:margin-right="0cm" fo:margin-top="0.101cm" fo:margin-bottom="0.169cm" fo:text-indent="0cm" style:auto-text-indent="false" style:page-number="auto"/> <@style@:paragraph-properties fo:margin-left="0cm" fo:margin-right="0cm" fo:margin-top="0.101cm" fo:margin-bottom="0.169cm" fo:text-indent="0cm" style:auto-text-indent="false" style:page-number="auto"/>
<@style@:text-properties style:font-name="DejaVu Sans" fo:font-size="10pt"/>
</@style@:style> </@style@:style>
<@style@:style @style@:name="Appy_Table_Content" @style@:display-name="Appy Table Contents" @style@:family="paragraph" <@style@:style @style@:name="Appy_Table_Content" @style@:display-name="Appy Table Contents" @style@:family="paragraph"
@style@:parent-style-name="AppyStandard" @style@:class="extra"> @style@:parent-style-name="AppyStandard" @style@:class="extra">
<@style@:paragraph-properties @fo@:margin-top="0cm" @fo@:margin-bottom="0cm" @text@:number-lines="false" @text@:line-number="0"/> <@style@:paragraph-properties @fo@:margin-top="0cm" @fo@:margin-bottom="0cm" @text@:number-lines="false" @text@:line-number="0"/>
<@style@:text-properties @fo@:font-size="8pt"/>
</@style@:style> </@style@:style>
<@style@:style @style@:name="Appy_Table_Heading" @style@:display-name="Appy Table Heading" @style@:family="paragraph" <@style@:style @style@:name="Appy_Table_Heading" @style@:display-name="Appy Table Heading" @style@:family="paragraph"
@style@:parent-style-name="Appy_Table_Contents" @style@:class="extra"> @style@:parent-style-name="Appy_Table_Contents" @style@:class="extra">
<@style@:paragraph-properties @fo@:text-align="center" @style@:justify-single-word="false" @text@:number-lines="false" <@style@:paragraph-properties @fo@:text-align="center" @style@:justify-single-word="false" @text@:number-lines="false" @text@:line-number="0"/>
@text@:line-number="0"/>
<@style@:text-properties @fo@:font-weight="bold" @style@:font-weight-asian="bold" @style@:font-weight-complex="bold"/> <@style@:text-properties @fo@:font-weight="bold" @style@:font-weight-asian="bold" @style@:font-weight-complex="bold"/>
</@style@:style> </@style@:style>

View file

@ -17,13 +17,13 @@ from appy.pod import *
# To which ODT tags do HTML tags correspond ? # To which ODT tags do HTML tags correspond ?
HTML_2_ODT = {'h1':'h', 'h2':'h', 'h3':'h', 'h4':'h', 'h5':'h', 'h6':'h', HTML_2_ODT = {'h1':'h', 'h2':'h', 'h3':'h', 'h4':'h', 'h5':'h', 'h6':'h',
'p':'p', 'div': 'p', 'b':'span', 'i':'span', 'strong':'span', 'p':'p', 'div': 'p', 'b':'span', 'i':'span', 'strong':'span', 'strike':'span',
'strike':'span', 'u':'span', 'em': 'span', 'sub': 'span', 's':'span', 'u':'span', 'em': 'span', 'sub': 'span', 'sup': 'span',
'sup': 'span', 'br': 'line-break'} 'br': 'line-break'}
DEFAULT_ODT_STYLES = {'b': 'podBold', 'strong':'podBold', 'i': 'podItalic', DEFAULT_ODT_STYLES = {'b': 'podBold', 'strong':'podBold', 'i': 'podItalic',
'u': 'podUnderline', 'strike': 'podStrike', 'u': 'podUnderline', 'strike': 'podStrike', 's': 'podStrike',
'em': 'podItalic', 'sup': 'podSup', 'sub':'podSub', 'em': 'podItalic', 'sup': 'podSup', 'sub':'podSub', 'td': 'podCell',
'td': 'podCell', 'th': 'podHeaderCell'} 'th': 'podHeaderCell'}
INNER_TAGS = ('b', 'strong', 'i', 'u', 'em', 'sup', 'sub', 'span') INNER_TAGS = ('b', 'strong', 'i', 'u', 'em', 'sup', 'sub', 'span')
TABLE_CELL_TAGS = ('td', 'th') TABLE_CELL_TAGS = ('td', 'th')
OUTER_TAGS = TABLE_CELL_TAGS + ('li',) OUTER_TAGS = TABLE_CELL_TAGS + ('li',)

View file

@ -245,7 +245,7 @@ def getTempFileName(prefix='', extension=''):
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
def executeCommand(cmd): def executeCommand(cmd):
'''Executes command p_cmd and returns the content of its stderr.''' '''Executes command p_cmd and returns the content of its stderr'''
childStdIn, childStdOut, childStdErr = os.popen3(cmd) childStdIn, childStdOut, childStdErr = os.popen3(cmd)
res = childStdErr.read() res = childStdErr.read()
childStdIn.close(); childStdOut.close(); childStdErr.close() childStdIn.close(); childStdOut.close(); childStdErr.close()

94
shared/zip.py Normal file
View file

@ -0,0 +1,94 @@
'''Functions for (un)zipping files'''
# ------------------------------------------------------------------------------
import os, os.path, zipfile, time
from appy.shared import mimeTypes
# ------------------------------------------------------------------------------
def unzip(f, folder, odf=False):
'''Unzips file p_f into p_folder. p_f can be any anything accepted by the
zipfile.ZipFile constructor. p_folder must exist.
If p_odf is True, p_f is considered to be an odt or ods file and this
function will return a dict containing the content of content.xml and
styles.xml from the zipped file.'''
zipFile = zipfile.ZipFile(f)
if odf: res = {}
else: res = None
for zippedFile in zipFile.namelist():
# Before writing the zippedFile into p_folder, 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 folder.
os.makedirs(os.path.join(folder, zippedFile.lstrip('/')))
else:
fileName = os.path.basename(zippedFile)
folderName = os.path.dirname(zippedFile)
fullFolderName = folder
if folderName:
fullFolderName = os.path.join(fullFolderName, folderName)
if not os.path.exists(fullFolderName):
os.makedirs(fullFolderName)
# Unzip the file in folder
if fileName:
fullFileName = os.path.join(fullFolderName, fileName)
f = open(fullFileName, 'wb')
fileContent = zipFile.read(zippedFile)
if odf and not folderName:
# content.xml and others may reside in subfolders. Get only the
# one in the root folder.
if fileName == 'content.xml':
res['content.xml'] = fileContent
elif fileName == 'styles.xml':
res['styles.xml'] = fileContent
elif fileName == 'mimetype':
res['mimetype'] = fileContent
f.write(fileContent)
f.close()
zipFile.close()
return res
# ------------------------------------------------------------------------------
def zip(f, folder, odf=False):
'''Zips the content of p_folder into the zip file whose (preferably)
absolute filename is p_f. If p_odf is True, p_folder is considered to
contain the standard content of an ODF file (content.xml,...). In this
case, some rules must be respected while building the zip (see below).'''
# Remove p_f if it exists
if os.path.exists(f): os.remove(f)
try:
zipFile = zipfile.ZipFile(f, 'w', zipfile.ZIP_DEFLATED)
except RuntimeError:
zipFile = zipfile.ZipFile(f, 'w')
# If p_odf is True, 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)".
if odf:
mimetypeFile = os.path.join(folder, 'mimetype')
# This file may not exist (presumably, ods files from Google Drive)
if not os.path.exists(mimetypeFile):
f = file(mimetypeFile, 'w')
f.write(mimeTypes[os.path.splitext(f)[-1][1:]])
f.close()
zipFile.write(mimetypeFile, 'mimetype', zipfile.ZIP_STORED)
for dir, dirnames, filenames in os.walk(folder):
for name in filenames:
folderName = dir[len(folder)+1:]
# For p_odf files, ignore file "mimetype" that was already inserted
if odf and (folderName == '') and (name == 'mimetype'): continue
zipFile.write(os.path.join(dir,name), os.path.join(folderName,name))
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(folder):]
zInfo = zipfile.ZipInfo("%s/" % folderName, time.localtime()[:6])
zInfo.external_attr = 48
zipFile.writestr(zInfo, '')
zipFile.close()
# ------------------------------------------------------------------------------