From 2bd3fe1eeb0a319a31f1fac9a91b3c877bd7526e Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Wed, 4 Jan 2012 18:03:46 +0100 Subject: [PATCH] appy.pod: xhtml2odt: ability to include images from img tags (anonymously). Non-anonymous solution for a Appy/Zope server only; function 'document': allow to specify size of images in cm or px, or via a 'style' tag; appy.gen: allow to upload images in ckeditor fields; improved error management. --- gen/__init__.py | 3 +- gen/installer.py | 3 +- gen/mixins/__init__.py | 60 +++++++++++++++---- gen/ui/edit.pt | 4 +- gen/ui/view.pt | 4 +- pod/doc_importers.py | 123 +++++++++++++++++++++++++++++++------- pod/imageNotFound.jpg | Bin 0 -> 861 bytes pod/renderer.py | 36 +++++++---- pod/styles.in.content.xml | 6 ++ pod/xhtml2odt.py | 21 +++++-- 10 files changed, 206 insertions(+), 54 deletions(-) create mode 100644 pod/imageNotFound.jpg diff --git a/gen/__init__.py b/gen/__init__.py index 0c4adf6..54e4897 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -2138,7 +2138,8 @@ class Pod(Type): stylesMapping = self.stylesMapping rendererParams = {'template': StringIO.StringIO(template.content), 'context': podContext, 'result': tempFileName, - 'stylesMapping': stylesMapping} + 'stylesMapping': stylesMapping, + 'imageResolver': tool.o.getApp()} if tool.unoEnabledPython: rendererParams['pythonWithUnoPath'] = tool.unoEnabledPython if tool.openOfficePort: diff --git a/gen/installer.py b/gen/installer.py index d4e3445..ca74531 100644 --- a/gen/installer.py +++ b/gen/installer.py @@ -21,7 +21,8 @@ homePage = ''' ''' errorPage = ''' - +

" % (ckNum, url) + self.REQUEST.RESPONSE.write(resp) + + def allows(self, permission, raiseError=False): '''Has the logged user p_permission on p_self ?''' - return self.getUser().has_permission(permission, self) + hasPermission = self.getUser().has_permission(permission, self) + if not hasPermission and raiseError: + from AccessControl import Unauthorized + raise Unauthorized + return hasPermission def getEditorInit(self, name): - '''Gets the Javascrit init code for displaying a rich editor for + '''Gets the Javascript init code for displaying a rich editor for field named p_name.''' - return "CKEDITOR.replace('%s', {toolbar: 'Appy'})" % name + return "CKEDITOR.replace('%s', {toolbar: 'Appy', filebrowserUploadUrl:"\ + "'%s/upload'})" % (name, self.absolute_url()) def getCalendarInit(self, name, years): '''Gets the Javascript init code for displaying a calendar popup for diff --git a/gen/ui/edit.pt b/gen/ui/edit.pt index ba4dce7..b5560d3 100644 --- a/gen/ui/edit.pt +++ b/gen/ui/edit.pt @@ -2,6 +2,7 @@ + confirmMsg request/confirmMsg | nothing;" + tal:on-error="structure python: tool.manageError(error)"> Include type-specific CSS and JS. + phase phaseInfo/name;" + tal:on-error="structure python: tool.manageError(error)"> diff --git a/pod/doc_importers.py b/pod/doc_importers.py index 0040cc7..c377d2c 100644 --- a/pod/doc_importers.py +++ b/pod/doc_importers.py @@ -17,10 +17,12 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA. # ------------------------------------------------------------------------------ -import os, os.path, time, shutil, struct, random +import os, os.path, time, shutil, struct, random, urlparse from appy.pod import PodError from appy.pod.odf_parser import OdfEnvironment +from appy.shared import mimeTypesExts from appy.shared.utils import FileWrapper +from appy.shared.dav import Resource # ------------------------------------------------------------------------------ FILE_NOT_FOUND = "'%s' does not exist or is not a file." @@ -32,28 +34,30 @@ PDF_TO_IMG_ERROR = 'A PDF file could not be converted into images. Please ' \ class DocImporter: '''Base class used for importing external content into a pod template (an image, another pod template, another odt document...''' - def __init__(self, content, at, format, tempFolder, ns, fileNames): + def __init__(self, content, at, format, renderer): self.content = content # If content is None, p_at tells us where to find it (file system path, # url, etc) self.at = at - # Ensure this path exists. - if at and not os.path.isfile(at): raise PodError(FILE_NOT_FOUND % at) + # Ensure this path exists, if it is a local path. + if at and not at.startswith('http') and not os.path.isfile(at): + raise PodError(FILE_NOT_FOUND % at) self.format = format self.res = u'' - self.ns = ns + self.renderer = renderer + self.ns = renderer.currentParser.env.namespaces # Unpack some useful namespaces - self.textNs = ns[OdfEnvironment.NS_TEXT] - self.linkNs = ns[OdfEnvironment.NS_XLINK] - self.drawNs = ns[OdfEnvironment.NS_DRAW] - self.svgNs = ns[OdfEnvironment.NS_SVG] - self.tempFolder = tempFolder + self.textNs = self.ns[OdfEnvironment.NS_TEXT] + self.linkNs = self.ns[OdfEnvironment.NS_XLINK] + self.drawNs = self.ns[OdfEnvironment.NS_DRAW] + self.svgNs = self.ns[OdfEnvironment.NS_SVG] + self.tempFolder = renderer.tempFolder self.importFolder = self.getImportFolder() # Create the import folder if it does not exist. if not os.path.exists(self.importFolder): os.mkdir(self.importFolder) self.importPath = self.getImportPath(at, format) # A link to the global fileNames dict (explained in renderer.py) - self.fileNames = fileNames + self.fileNames = renderer.fileNames if at: # Move the file within the ODT, if it is an image and if this image # has not already been imported. @@ -84,7 +88,10 @@ class DocImporter: '''Gets the path name of the file to dump on disk (within the ODT for images, in a temp folder for docs).''' if not format: - format = os.path.splitext(at)[1][1:] + if at.startswith('http'): + format = '' # We will know it only after the HTTP GET. + else: + format = os.path.splitext(at)[1][1:] fileName = 'f.%d.%f.%s' % (random.randint(0,10), time.time(), format) return os.path.abspath('%s/%s' % (self.importFolder, fileName)) @@ -136,8 +143,7 @@ class PdfImporter(DocImporter): nextImage = '%s/%s%d.jpg' % (imagesFolder, self.imagePrefix, i) if os.path.exists(nextImage): # Use internally an Image importer for doing this job. - imgImporter = ImageImporter(None, nextImage, 'jpg', - self.tempFolder, self.ns, self.fileNames) + imgImporter =ImageImporter(None, nextImage, 'jpg',self.renderer) imgImporter.setAnchor('paragraph') self.res += imgImporter.run() os.remove(nextImage) @@ -199,17 +205,70 @@ class ImageImporter(DocImporter): # Yes! i = importPath.rfind(self.pictFolder) + 1 return importPath[:i] + imagePath - # If I am here, the image has not already been imported: copy it. - shutil.copy(at, importPath) + # The image has not already been imported: copy it. + if not at.startswith('http'): + shutil.copy(at, importPath) + return importPath + # The image must be retrieved via a URL. Try to perform a HTTP GET. + response = Resource(at).get() + if response.code == 200: + # At last, I can get the file format. + self.format = mimeTypesExts[response.headers['Content-Type']] + importPath += self.format + f = file(importPath, 'wb') + f.write(response.body) + f.close() + return importPath + # The HTTP GET did not work, maybe for security reasons (we probably + # have no permission to get the file). But maybe the URL was a local + # one, from an application server running this POD code. In this case, + # if an image resolver has been given to POD, use it to retrieve the + # image. + imageResolver = self.renderer.imageResolver + if not imageResolver: + # Return some default image explaining that the image wasn't found. + import appy.pod + podFolder = os.path.dirname(appy.pod.__file__) + img = os.path.join(podFolder, 'imageNotFound.jpg') + self.format = 'jpg' + importPath += self.format + f = file(img) + imageContent = f.read() + f.close() + f = file(importPath, 'wb') + f.write(imageContent) + f.close() + else: + # The imageResolver is a Zope application. From it, we will + # retrieve the object on which the image is stored and get + # the file to download. + urlParts = urlparse.urlsplit(at) + path = urlParts[2][1:] + obj = imageResolver.unrestrictedTraverse(path.split('/')[:-1]) + zopeFile = getattr(obj, urlParts[3].split('=')[1]) + appyFile = FileWrapper(zopeFile) + self.format = mimeTypesExts[appyFile.mimeType] + importPath += self.format + appyFile.dump(importPath) return importPath - def setImageInfo(self, anchor, wrapInPara, size): + def setImageInfo(self, anchor, wrapInPara, size, sizeUnit, style): # Initialise anchor if anchor not in self.anchorTypes: raise PodError(self.WRONG_ANCHOR % str(self.anchorTypes)) self.anchor = anchor self.wrapInPara = wrapInPara self.size = size + self.sizeUnit = sizeUnit + # Put CSS attributes from p_style in a dict. + self.cssAttrs = {} + for attr in style.split(';'): + if not attr.strip(): continue + name, value = attr.strip().split(':') + value = value.strip() + if value.endswith('px'): value = value[:-2] + if value.isdigit(): value=int(value) + self.cssAttrs[name.strip()] = value def run(self): # Some shorcuts for the used xml namespaces @@ -222,19 +281,37 @@ class ImageImporter(DocImporter): i = self.importPath.rfind(self.pictFolder) imagePath = self.importPath[i+1:].replace('\\', '/') self.fileNames[imagePath] = self.at - # Compute image size, or retrieve it from self.size if given + # Retrieve image size from self.size. + width = height = None if self.size: width, height = self.size - else: + if self.sizeUnit == 'px': + # Convert it to cm + width = float(width) / pxToCm + height = float(height) / pxToCm + # Override self.size if 'height' or 'width' is found in self.cssAttrs + if 'width' in self.cssAttrs: + width = float(self.cssAttrs['width']) / pxToCm + if 'height' in self.cssAttrs: + height = float(self.cssAttrs['height']) / pxToCm + # If width and/or height is missing, compute it. + if not width or not height: width, height = getSize(self.importPath, self.format) if width != None: size = ' %s:width="%fcm" %s:height="%fcm"' % (s, width, s, height) else: size = '' - image = '<%s:frame %s:name="%s" %s:z-index="0" %s:anchor-type="%s"%s>' \ - '<%s:image %s:type="simple" %s:show="embed" %s:href="%s" ' \ - '%s:actuate="onLoad"/>' % (d, d, imageName, d, t, \ - self.anchor, size, d, x, x, x, imagePath, x, d) + if 'float' in self.cssAttrs: + floatValue = self.cssAttrs['float'].capitalize() + styleInfo = '%s:style-name="podImage%s" ' % (d, floatValue) + self.anchor = 'char' + else: + styleInfo = '' + image = '<%s:frame %s%s:name="%s" %s:z-index="0" ' \ + '%s:anchor-type="%s"%s><%s:image %s:type="simple" ' \ + '%s:show="embed" %s:href="%s" %s:actuate="onLoad"/>' \ + '' % (d, styleInfo, d, imageName, d, t, self.anchor, + size, d, x, x, x, imagePath, x, d) if hasattr(self, 'wrapInPara') and self.wrapInPara: image = '<%s:p>%s' % (t, image, t) self.res += image diff --git a/pod/imageNotFound.jpg b/pod/imageNotFound.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8ee2e5c68c46ccffd7e134878ce2780e9b6ea686 GIT binary patch literal 861 zcmex=>2(}6%-H@WJD(vKQbM8oSgooCn``oIwy7OY3qqM9b41vq}p^b?rzrt)-q0&oQmH*btQkT_8)3G?pC}l&x*(4TI0R1 z%-e;%TwxP^V)gS#RmpW*!dU$d7>Uopr#@zuIW z^H#(ZbR97zCf+|B8Ltse+BHG5>nsEx9fH=iG_E zXKr6Ej11*XXO@xb zvJ*Q!R!`#ir&>M##*N1P+Ly9SjV@Ul@7g>&H+e6U&HK8~VQQv4Grruo{r^n>aw9Cb literal 0 HcmV?d00001 diff --git a/pod/renderer.py b/pod/renderer.py index d0cb701..8261b17 100644 --- a/pod/renderer.py +++ b/pod/renderer.py @@ -94,7 +94,8 @@ STYLES_POD_FONTS = '<@style@:font-face @style@:name="PodStarSymbol" ' \ class Renderer: def __init__(self, template, context, result, pythonWithUnoPath=None, ooPort=2002, stylesMapping={}, forceOoCall=False, - finalizeFunction=None, overwriteExisting=False): + finalizeFunction=None, overwriteExisting=False, + imageResolver=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 @@ -128,7 +129,13 @@ class Renderer: - 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.''' + already exists. + + - 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 @@ -143,6 +150,7 @@ class Renderer: self.forceOoCall = forceOoCall self.finalizeFunction = finalizeFunction self.overwriteExisting = overwriteExisting + 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 @@ -235,13 +243,12 @@ class Renderer: 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 can only be a chunk of XHTML. So we must surround it a # tag in order to get a XML-compliant file (we need a root tag). if xhtmlString == None: xhtmlString = '' xhtmlContent = '

%s

' % xhtmlString return Xhtml2OdtConverter(xhtmlContent, encoding, self.stylesManager, - stylesMapping, ns).run() + stylesMapping, self).run() def renderText(self, text, encoding='utf-8', stylesMapping={}): '''Method that can be used (under the name 'text') into a pod template @@ -262,7 +269,8 @@ class Renderer: imageFormats = ('png', 'jpeg', 'jpg', 'gif') ooFormats = ('odt',) def importDocument(self, content=None, at=None, format=None, - anchor='as-char', wrapInPara=True, size=None): + anchor='as-char', wrapInPara=True, size=None, + sizeUnit='cm', style=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 @@ -274,9 +282,14 @@ class Renderer: * 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 centimeters. If not - specified, size will be computed from image info.''' - ns = self.currentParser.env.namespaces + (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. + ''' importer = None # Is there someting to import? if not content and not at: @@ -297,16 +310,17 @@ class Renderer: if format in self.ooFormats: importer = OdtImporter self.forceOoCall = True - elif format in self.imageFormats: + 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 else: raise PodError(DOC_WRONG_FORMAT % format) - imp = importer(content, at, format, self.tempFolder, ns, self.fileNames) + imp = importer(content, at, format, self) # Initialise image-specific parameters - if isImage: imp.setImageInfo(anchor, wrapInPara, size) + if isImage: imp.setImageInfo(anchor, wrapInPara, size, sizeUnit, style) res = imp.run() return res diff --git a/pod/styles.in.content.xml b/pod/styles.in.content.xml index ba40861..6b30c18 100644 --- a/pod/styles.in.content.xml +++ b/pod/styles.in.content.xml @@ -113,3 +113,9 @@ <@style@:list-level-properties @text@:space-before="2.5in" @text@:min-label-width="0.25in"/> +<@style@:style @style@:name="podImageLeft" @style@:family="graphic" @style@:parent-style-name="Graphics"> + <@style@:graphic-properties @style@:run-through="foreground" @style@:wrap="parallel" @style@:number-wrapped-paragraphs="no-limit" @style@:wrap-contour="false" @style@:vertical-pos="top" @style@:vertical-rel="paragraph" @style@:horizontal-pos="left" @style@:horizontal-rel="paragraph" @style@:mirror="none" @fo@:clip="rect(0cm, 0.3cm, 0cm, 0cm)"/> + +<@style@:style @style@:name="podImageRight" @style@:family="graphic" @style@:parent-style-name="Graphics"> + <@style@:graphic-properties @style@:run-through="foreground" @style@:wrap="parallel" @style@:number-wrapped-paragraphs="no-limit" @style@:wrap-contour="false" @style@:vertical-pos="top" @style@:vertical-rel="paragraph" @style@:horizontal-pos="right" @style@:horizontal-rel="paragraph" @style@:mirror="none" @fo@:clip="rect(0cm, 0.3cm, 0cm, 0cm)"/> + diff --git a/pod/xhtml2odt.py b/pod/xhtml2odt.py index aa636c6..0e4950b 100644 --- a/pod/xhtml2odt.py +++ b/pod/xhtml2odt.py @@ -214,16 +214,18 @@ class XhtmlEnvironment(XmlEnvironment): 'ul_kwn': 'podBulletItemKeepWithNext', 'ol_kwn': 'podNumberItemKeepWithNext'} listStyles = {'ul': 'podBulletedList', 'ol': 'podNumberedList'} - def __init__(self, ns): + def __init__(self, renderer): XmlEnvironment.__init__(self) + self.renderer = renderer + self.ns = renderer.currentParser.env.namespaces self.res = u'' self.currentContent = u'' self.currentElements = [] # Stack of currently walked elements self.currentLists = [] # Stack of currently walked lists (ul or ol) self.currentTables = [] # Stack of currently walked tables - self.textNs = ns[OdfEnvironment.NS_TEXT] - self.linkNs = ns[OdfEnvironment.NS_XLINK] - self.tableNs = ns[OdfEnvironment.NS_TABLE] + self.textNs = self.ns[OdfEnvironment.NS_TEXT] + self.linkNs = self.ns[OdfEnvironment.NS_XLINK] + self.tableNs = self.ns[OdfEnvironment.NS_TABLE] self.ignore = False # Will be True when parsing parts of the XHTML that # must be ignored. @@ -445,6 +447,12 @@ class XhtmlParser(XmlParser): e.dumpString(' %s:number-columns-spanned="%s"' % \ (e.tableNs, attrs['colspan'])) e.dumpString('>') + elif elem == 'img': + style = None + if attrs.has_key('style'): style = attrs['style'] + imgCode = e.renderer.importDocument(at=attrs['src'], + wrapInPara=False, style=style) + e.dumpString(imgCode) elif elem in IGNORABLE_TAGS: e.ignore = True @@ -483,7 +491,8 @@ class XhtmlParser(XmlParser): class Xhtml2OdtConverter: '''Converts a chunk of XHTML into a chunk of ODT.''' def __init__(self, xhtmlString, encoding, stylesManager, localStylesMapping, - ns): + renderer): + self.renderer = renderer self.xhtmlString = xhtmlString self.encoding = encoding # Todo: manage encoding that is not utf-8 self.stylesManager = stylesManager @@ -491,7 +500,7 @@ class Xhtml2OdtConverter: self.globalStylesMapping = stylesManager.stylesMapping self.localStylesMapping = localStylesMapping self.odtChunk = None - self.xhtmlParser = XhtmlParser(XhtmlEnvironment(ns), self) + self.xhtmlParser = XhtmlParser(XhtmlEnvironment(renderer), self) def run(self): self.xhtmlParser.parse(self.xhtmlString)