Commit bdcdd216 authored by Fabien Devaux's avatar Fabien Devaux

Added inclusion of OLE documents and pictures.

See readme.txt for details.


git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@6328 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 2ee54c09
......@@ -40,6 +40,13 @@ from urllib import quote
from Globals import InitializeClass, DTMLFile, get_request
from AccessControl import ClassSecurityInfo
from OOoUtils import OOoBuilder
from zipfile import ZipFile, ZIP_DEFLATED
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
import re
import itertools
from zLOG import LOG
......@@ -62,6 +69,10 @@ def addOOoTemplate(self, id, title="", REQUEST=None):
"""
# add actual object
id = self._setObject(id, OOoTemplate(id, title))
file = REQUEST.form.get('file')
if file.filename:
# Get the template in the associated context and upload the file
getattr(self,id).pt_upload(REQUEST, file)
# respond to the add_and_edit button if necessary
add_and_edit(self, id, REQUEST)
return ''
......@@ -91,6 +102,14 @@ class OOoTemplate(ZopePageTemplate):
meta_type = "ERP5 OOo Template"
icon = "www/OOo.png"
# NOTE: 100 is just pure random starting number
# it won't influence the code at all
document_counter = itertools.count(100)
# Every linked OLE document is in a directory starting with 'Obj'
_OLE_directory_prefix = 'Obj'
# every OOo document have a content-type starting like this
_OOo_content_type_root = 'application/vnd.sun.xml.'
# Declarative Security
security = ClassSecurityInfo()
......@@ -120,7 +139,12 @@ class OOoTemplate(ZopePageTemplate):
formSettings = PageTemplateFile('www/formSettings', globals(),
__name__='formSettings')
formSettings._owner = None
def __init__(self,*args,**kw):
ZopePageTemplate.__init__(self,*args,**kw)
# we store the attachments of the uploaded document
self.OLE_documents_zipstring = None
def pt_upload(self, REQUEST, file=''):
"""Replace the document with the text in file."""
if SUPPORTS_WEBDAV_LOCKS and self.wl_isLocked():
......@@ -133,6 +157,25 @@ class OOoTemplate(ZopePageTemplate):
if file.startswith("PK") : # FIXME: this condition is probably not enough
# this is a OOo zip file, extract the content
builder = OOoBuilder(file)
attached_files_list = [n for n in builder.getNameList()
if n.startswith(self._OLE_directory_prefix)
or n.startswith('Pictures')
or n == 'META-INF/manifest.xml' ]
# destroy a possibly pre-existing OLE document set
if self.OLE_documents_zipstring:
self.OLE_documents_zipstring = None
# create a zip archive and store it
if attached_files_list:
memory_file = StringIO()
try:
zf = ZipFile(memory_file, mode='w', compression=ZIP_DEFLATED)
except RuntimeError:
zf = ZipFile(memory_file, mode='w')
for attached_file in attached_files_list:
zf.writestr(attached_file, builder.extract(attached_file) )
zf.close()
memory_file.seek(0)
self.OLE_documents_zipstring = memory_file.read()
self.content_type = builder.getMimeType()
file = builder.prepareContentXml()
......@@ -154,6 +197,183 @@ class OOoTemplate(ZopePageTemplate):
% '<br>'.join(self._v_warnings))
return self.formSettings(manage_tabs_message=message)
def _resolvePath(self, path):
return self.getPortalObject().unrestrictedTraverse(path)
def renderIncludes(self, text, sub_document=None):
attached_files_dict = {}
arguments_re = re.compile('(\w+)\s*=\s*"(.*?)"\s*',re.DOTALL)
def getLengthInfos( opts_dict, opts_names ):
ret = []
for opt_name in opts_names:
try:
val = opts_dict[opt_name]
if val.endswith('cm'):
val = val[:-2]
val = float( val )
except (ValueError, KeyError):
val = None
ret.append(val)
return ret
def replaceIncludes(match):
options_dict = dict( style="fr1", x="0cm", y="0cm" )
options_dict.update( dict(arguments_re.findall( match.group(1) )) )
document = self._resolvePath( options_dict['path'] )
document_text = document.read()
if 'type' not in options_dict:
options_dict['type'] = document.content_type
else: # type passed in short form as an attribute
options_dict['type'] = self._OOo_content_type_root + options_dict['type']
w, h, x, y = getLengthInfos( options_dict , ('width', 'height', 'x', 'y') )
# Set defaults
if w is None:
w = 10.0
if h is None:
h = 10.0
if x is None:
x = 0.0
if y is None:
y = 0.0
actual_idx = self.document_counter.next()
dir_name = '%s%d'%(self._OLE_directory_prefix,actual_idx)
if sub_document: # sub-document means sub-directory
dir_name = sub_document+'/'+dir_name
try:
temp_builder = OOoBuilder(getattr(document,document.ooo_stylesheet))
stylesheet = temp_builder.extract('styles.xml')
except AttributeError:
stylesheet = None
sub_attached_files_dict = {}
if 'office:include' in document_text: # small optimisation to avoid recursion if possible
(document_text, sub_attached_files_dict ) = self.renderIncludes(document_text, dir_name)
# View* = writer
# Visible* = calc
settings_text = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE office:document-settings PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "office.dtd">
<office:document-settings xmlns:office="http://openoffice.org/2000/office"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:config="http://openoffice.org/2001/config" office:version="1.0">
<office:settings>
<config:config-item-set config:name="view-settings">
<config:config-item config:name="ViewAreaTop" config:type="int">0</config:config-item>
<config:config-item config:name="ViewAreaLeft" config:type="int">0</config:config-item>
<config:config-item config:name="ViewAreaWidth" config:type="int">%(w)d</config:config-item>
<config:config-item config:name="ViewAreaHeight" config:type="int">%(h)d</config:config-item>
<config:config-item config:name="VisibleAreaTop" config:type="int">0</config:config-item>
<config:config-item config:name="VisibleAreaLeft" config:type="int">0</config:config-item>
<config:config-item config:name="VisibleAreaWidth" config:type="int">%(w)d</config:config-item>
<config:config-item config:name="VisibleAreaHeight" config:type="int">%(h)d</config:config-item>
</config:config-item-set>
</office:settings>
</office:document-settings>"""%dict( w=int(w*1000) , h=int(h*1000) ) # convert from 10^-2 (centimeters) to 10^-5
attached_files_dict[dir_name] = dict(document = document_text,
doc_type = options_dict['type'], stylesheet = stylesheet )
attached_files_dict[dir_name+'/settings.xml'] = dict( document = settings_text,
doc_type = 'text/xml' )
attached_files_dict.update(sub_attached_files_dict )
# add a paragraph with the OLE document in it
# The dir_name is relative here, extract the last path component
replacement = """
<draw:object draw:style-name="%s" draw:name="ERP5IncludedObject%d"
text:anchor-type="paragraph" svg:x="%.3fcm" svg:y="%.3fcm"
svg:width="%.3fcm" svg:height="%.3fcm" xlink:href="#./%s"
xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/>
"""%(options_dict['style'], actual_idx, x, y, w, h, dir_name.split('/')[-1])
if not self.content_type.endswith('draw'):
replacement = '<text:p text:style-name="Standard">'+replacement+'</text:p>'
return replacement
def replaceIncludesImg(match):
options_dict = dict( x='0cm', y='0cm', style="fr1" )
options_dict.update( dict(arguments_re.findall( match.group(1) )) )
picture = self._resolvePath( options_dict['path'] )
# "standard" filetype == Image or File , for ERP objects the
# manipulations are different
is_standard_filetype = True
if not hasattr(picture,'data') or callable(picture.content_type):
is_standard_filetype = False
if is_standard_filetype:
picture_data = picture.data
else:
picture_data = picture.Base_download()
# fetch the content-type of the picture (generally guessed by zope)
if 'type' not in options_dict:
if is_standard_filetype:
options_dict['type'] = picture.content_type
else:
options_dict['type'] = picture.content_type()
if '/' not in options_dict['type']:
options_dict['type'] = 'image/' + options_dict['type']
w, h, maxwidth, maxheight = getLengthInfos( options_dict, ('width','height','maxwidth','maxheight') )
try:
aspect_ratio = float(picture.width) / float(picture.height)
except TypeError:
aspect_ratio = float(picture.width()) / float(picture.height())
# fix a default value and correct the aspect
if h in None:
if w is None:
w = 10.0
h = w / aspect_ratio
elif w is None:
w = h * aspect_ratio
# picture is too large
if maxwidth and maxwidth < w:
w = maxwidth
h = w / aspect_ratio
if maxheight and maxheight < h:
h = maxheight
w = h * aspect_ratio
actual_idx = self.document_counter.next()
pic_name = 'Pictures/picture%d.%s'%(actual_idx, options_dict['type'].split('/')[-1])
if sub_document: # sub-document means sub-directory
pic_name = sub_document+'/'+pic_name
attached_files_dict[pic_name] = dict(
document = picture_data,
doc_type = options_dict['type']
)
# XXX: Pictures directory not managed (seems facultative)
# <manifest:file-entry manifest:media-type="" manifest:full-path="ObjBFE4F50D/Pictures/"/>
replacement = """<draw:image draw:style-name="%s" draw:name="ERP5Image%d"
text:anchor-type="paragraph" svg:x="%s" svg:y="%s"
svg:width="%.3fcm" svg:height="%.3fcm" xlink:href="#Pictures/%s"
xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/>
"""%(options_dict['style'], actual_idx,
options_dict['x'], options_dict['y'],
w, h,
pic_name.split('/')[-1] )
if not self.content_type.endswith('draw'):
replacement = '<text:p text:style-name="Standard">'+replacement+'</text:p>'
return replacement
# NOTE: (?s) at the end is for including '\n' when matching '.'
# It's an equivalent to DOTALL option passing (but sub can't get options parameter)
text = re.sub('<\s*office:include_img\s+(.*?)\s*/\s*>(?s)', replaceIncludesImg, text)
text = re.sub('<\s*office:include\s+(.*?)\s*/\s*>(?s)', replaceIncludes, text)
return (text, attached_files_dict)
# Proxy method to PageTemplate
def pt_render(self, source=0, extra_context={}):
# Get request
......@@ -170,6 +390,31 @@ class OOoTemplate(ZopePageTemplate):
# And render page template
doc_xml = ZopePageTemplate.pt_render(self, source=source, extra_context=extra_context)
# Replace the includes
(doc_xml,attachments_dict) = self.renderIncludes(doc_xml)
try:
default_styles_text = ooo_builder.extract('styles.xml')
except AttributeError:
default_styles_text = None
# Add the associated files
for dir_name, document_dict in attachments_dict.iteritems():
# Special case : the document is an OOo one
if document_dict['doc_type'].startswith(self._OOo_content_type_root):
ooo_builder.addFileEntry(full_path = dir_name,
media_type = document_dict['doc_type'] )
ooo_builder.addFileEntry(full_path = dir_name+'/content.xml',
media_type = 'text/xml',content = document_dict['document'] )
styles_text = default_styles_text
if document_dict.has_key('stylesheet') and document_dict['stylesheet']:
styles_text = document_dict['stylesheet']
if styles_text:
ooo_builder.addFileEntry(full_path = dir_name+'/styles.xml',
media_type = 'text/xml',content = styles_text )
else: # Generic case
ooo_builder.addFileEntry(full_path=dir_name,
media_type=document_dict['doc_type'], content = document_dict['document'] )
# Get request and batch_mode
batch_mode = extra_context.get('batch_mode', 0)
......@@ -180,6 +425,15 @@ class OOoTemplate(ZopePageTemplate):
# Replace content.xml in master openoffice template
ooo_builder.replace('content.xml', doc_xml)
# If the file has embedded OLE documents, restore it
if self.OLE_documents_zipstring:
additional_builder = OOoBuilder( self.OLE_documents_zipstring )
for name in additional_builder.getNameList():
ooo_builder.replace(name, additional_builder.extract(name) )
# Update the META informations
ooo_builder.updateManifest()
# Produce final result
ooo = ooo_builder.render(self.title or self.id)
......@@ -218,5 +472,5 @@ class FSOOoTemplate(FSPageTemplate, OOoTemplate):
InitializeClass(FSOOoTemplate)
registerFileExtension('ooot', FSOOoTemplate)
registerMetaType('ERP5 OOo Template', FSOOoTemplate)
registerMetaType(OOoTemplate.meta_type, FSOOoTemplate)
......@@ -34,7 +34,10 @@ from xml.dom import Node
from AccessControl import ClassSecurityInfo
from Globals import InitializeClass, get_request
from zipfile import ZipFile, ZIP_DEFLATED
from StringIO import StringIO
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
from zLOG import LOG
import imghdr
import random
......@@ -72,6 +75,7 @@ class OOoBuilder:
else :
self._document = StringIO(document)
self._image_count = 0
self._manifest_additions_list = []
security.declarePublic('replace')
def replace(self, filename, stream):
......@@ -83,6 +87,13 @@ class OOoBuilder:
zf = ZipFile(self._document, mode='a', compression=ZIP_DEFLATED)
except RuntimeError:
zf = ZipFile(self._document, mode='a')
try:
# remove the file first if it exists
fi = zf.getinfo(filename)
zf.filelist.remove( fi )
except KeyError:
# This is a new file
pass
zf.writestr(filename, stream)
zf.close()
......@@ -97,6 +108,16 @@ class OOoBuilder:
zf = ZipFile(self._document, mode='r')
return zf.read(filename)
security.declarePublic('getNameList')
def getNameList(self):
try:
zf = ZipFile(self._document, mode='r', compression=ZIP_DEFLATED)
except RuntimeError:
zf = ZipFile(self._document, mode='r')
li = zf.namelist()
zf.close()
return li
security.declarePublic('getMimeType')
def getMimeType(self):
return self.extract('mimetype')
......@@ -124,6 +145,40 @@ class OOoBuilder:
tal:attributes='dummy python:request.RESPONSE.setHeader("Content-Type", "text/html;; charset=utf-8")'
office:version='1.0'""")
"""
"""
security.declarePublic('addFileEntry')
def addFileEntry(self, full_path, media_type, content=None):
""" Add a file entry to the manifest and possibly is content """
self.addManifest(full_path, media_type)
if content:
self.replace(full_path, content)
security.declarePublic('addManifest')
def addManifest(self, full_path, media_type):
""" Add a path to the manifest """
li = '<manifest:file-entry manifest:media-type="%s" manifest:full-path="%s"/>'%(media_type, full_path)
self._manifest_additions_list.append(li)
security.declarePublic('updateManifest')
def updateManifest(self):
""" Add a path to the manifest """
MANIFEST_FILENAME = 'META-INF/manifest.xml'
meta_infos = self.extract(MANIFEST_FILENAME)
# prevent some duplicates
for meta_line in meta_infos.split('\n'):
for new_meta_line in self._manifest_additions_list:
if meta_line.strip() == new_meta_line:
self._manifest_additions_list.remove(new_meta_line)
# add the new lines
self._manifest_additions_list.append('</manifest:manifest>')
meta_infos = meta_infos.replace( self._manifest_additions_list[-1], '\n'.join(self._manifest_additions_list) )
self.replace(MANIFEST_FILENAME, meta_infos)
self._manifest_additions_list = []
security.declarePublic('addImage')
def addImage(self, image, format='png'):
"""
......@@ -296,14 +351,18 @@ class OOoParser:
# List all embedded spreadsheets
emb_objects = self.oo_content_dom.getElementsByTagName("draw:object")
for embedded in emb_objects:
document = embedded.getAttributeNS(self.ns["xlink"], "href")
if document:
try:
object_content = self.reader.fromString(self.oo_files[document[3:] + '/content.xml'])
for table in object_content.getElementsByTagName("table:table"):
spreadsheets.append(table)
except:
pass
document = embedded.getAttributeNS(self.ns["xlink"], "href")
if document:
try:
object_content = self.reader.fromString(self.oo_files[document[3:] + '/content.xml'])
tables = object_content.getElementsByTagName("table:table")
if tables:
for table in tables:
spreadsheets.append(table)
else: # XXX: insert the link to OLE document ?
pass
except:
pass
return spreadsheets
......
......@@ -9,7 +9,7 @@ OOo Templates allow to generate dynamic OpenOffice.org documents based
on templates.
</p>
<form action="addOOoTemplate" method="POST">
<form action="addOOoTemplate" enctype="multipart/form-data" method="POST">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
......@@ -34,6 +34,17 @@ on templates.
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-optional">
File
</div>
</td>
<td align="left" valign="top">
<input type="file" name="file" size="25" value="" />
</td>
</tr>
<tr>
<td align="left" valign="top">
</td>
......
......@@ -10,6 +10,34 @@ OOo Template
Rendering consists in replacing content.xml of the original OOo document
with the content.xml generated by the OOo Template.
Special tags for including content:
- <office:include>
Allow you to include another document in the current template (as an OLE attachment)
You must specify at least the path (can be either a single name or a path name using "/")
and the type of the document (generally calc or writer), the default is zope's content-type.
The size is always specified in centimeters (with attached "cm" suffix or not).
You can specify the style (defined in the sylesheet) with the "style" option.
You can also pass "x" and "y" attributes for positioning, it's mainly useful for draw documents.
Example:
<office:include type="calc" width="10.100cm" height="16.000cm" path="agenda" />
<office:include width="15" height="20" path="/reports/my_report" />
<office:include path="foo" />
- <office:include_img>
Not unlike <office:include>, allows you to include a picture document, refer to
the <office:include> part for details.
The optional "type" attribute specifies the picture format ; you can either
pass a full value ("image/jpeg") or the short version ("jpeg").
You can also pass position parameters with "x" and "y" attributes.
The maxwidth and maxheight parameters are useful to set constraints.
The aspect ratio information try to be kept (if you set only one size
or if a constraint is applied).
Example:
<office:include x="5cm" y="1cm" path="foo" />
Tips:
- it is possible to embed images by calling oo_builder.addImage(image)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment