Commit 036a066d authored by Rafael Monnerat's avatar Rafael Monnerat

Fix SVG to PNG conversion when a xlink:href uses a URL

Transform the URL into Data URI in order to avoid ImageMagick and
rsvg-convert limitations. This API should be dropped as soon those external
tools can handle remote URLs (ie.: http, https ...) into SVG definitions.

It was included defensive code and a lot of tests to prevent unexpected
outcomes, like raise ConversionError if the download failures and do
not parse empty content.
parent 48ee69f7
......@@ -51,6 +51,8 @@ from OFS.Image import Image as OFSImage
from OFS.Image import getImageInfo
from zLOG import LOG, WARNING
from Products.ERP5Type.ImageUtil import transformUrlToDataURI
# import mixin
from Products.ERP5.mixin.text_convertable import TextConvertableMixin
......@@ -330,6 +332,10 @@ class Image(TextConvertableMixin, File, OFSImage):
else:
parameter_list.append('-')
data = str(self.getData())
if self.getContentType() == "image/svg+xml":
data = transformUrlToDataURI(data)
process = subprocess.Popen(parameter_list,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
......@@ -338,7 +344,7 @@ class Image(TextConvertableMixin, File, OFSImage):
try:
# XXX: The only portable way is to pass what stdin.write can accept,
# which is a string for PIPE.
image, err = process.communicate(str(self.getData()))
image, err = process.communicate(data)
finally:
del process
if image:
......
......@@ -39,6 +39,7 @@ from Testing import ZopeTestCase
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase,\
_getConversionServerDict
from Products.ERP5Type.tests.utils import FileUpload, createZODBPythonScript
from Products.ERP5.Document.Document import ConversionError
try:
from PIL import Image
......@@ -820,7 +821,7 @@ return True
module = portal.getDefaultModule(portal_type=portal_type)
upload_file = makeFileUpload('user-TESTSVG-CASE-FULLURL-TEMPLATE.svg')
svg_content = upload_file.read().replace("REPLACE_THE_URL_HERE",
makeFilePath("user-TESTSVG-BACKGROUND-IMAGE.png"))
"file://" + makeFilePath("user-TESTSVG-BACKGROUND-IMAGE.png"))
# Add image using data instead file this time as it is not the goal of
# This test assert this topic.
......@@ -843,6 +844,69 @@ return True
"Conversion from svg to png create one too small image, " + \
"so it failed to download the image. (%s >= 100)" % difference_value)
def _testImageConversionFromSVGToPNG_broken_url(self, portal_type="Image"):
""" Test Convert one broken SVG into PNG. The expected outcome is a
conversion error when an SVG contains one unreacheble xlink:href like.
at the url of the image tag. ie:
<image xlink:href="http://soidjsoidjqsoijdqsoidjqsdoijsqd.idjsijds/../user-XXX-XXX"
This is not used by ERP5 in production, but this is way that
prooves that conversion from SVG to PNG can use external images.
"""
portal = self.portal
module = portal.getDefaultModule(portal_type=portal_type)
upload_file = makeFileUpload('user-TESTSVG-CASE-FULLURL-TEMPLATE.svg')
svg_content = upload_file.read().replace("REPLACE_THE_URL_HERE",
"http://soidjsoidjqsoijdqsoidjqsdoijsqd.idjsijds/../user-XXX-XXX")
upload_file = makeFileUpload('user-TESTSVG-CASE-FULLURL-TEMPLATE.svg')
svg2_content = upload_file.read().replace("REPLACE_THE_URL_HERE",
"https://www.erp5.com/usXXX-XXX")
# Add image using data instead file this time as it is not the goal of
# This test assert this topic.
image = module.newContent(portal_type=portal_type,
data=svg_content,
filename=upload_file.filename,
content_type="image/svg+xml",
reference="NXD-DOCYMENT")
# Add image using data instead file this time as it is not the goal of
# This test assert this topic.
image2 = module.newContent(portal_type=portal_type,
data=svg2_content,
filename=upload_file.filename,
content_type="image/svg+xml",
reference="NXD-DOCYMENT2")
image.publish()
image2.publish()
transaction.commit()
self.tic()
self.assertEquals(image.getContentType(), 'image/svg+xml')
self.assertEquals(image2.getContentType(), 'image/svg+xml')
self.assertRaises(ConversionError, image.convert, "png")
self.assertRaises(ConversionError, image2.convert, "png")
def _testImageConversionFromSVGToPNG_empty_file(self, portal_type="Image"):
""" Test Convert one empty SVG into PNG. The expected outcome is ???
"""
portal = self.portal
module = portal.getDefaultModule(portal_type=portal_type)
# Add image using data instead file this time as it is not the goal of
# This test assert this topic.
image = module.newContent(portal_type=portal_type,
content_type="image/svg+xml",
reference="NXD-DOCYMENT")
image.publish()
transaction.commit()
self.tic()
self.assertEquals(image.getContentType(), 'image/svg+xml')
self.assertRaises(ConversionError, image.convert, "png")
def test_ImageConversionFromSVGToPNG_embeeded_data(self):
""" Test Convert one SVG Image with an image with the data
at the url of the image tag.ie:
......@@ -864,6 +928,36 @@ return True
"""
self._testImageConversionFromSVGToPNG("Web Page")
def test_ImageConversionFromSVGToPNG_broken_url(self):
""" Test Convert one SVG Image with an broken image href
"""
self._testImageConversionFromSVGToPNG_broken_url("Image")
def test_FileConversionFromSVGToPNG_broken_url(self):
""" Test Convert one SVG Image with an broken image href
"""
self._testImageConversionFromSVGToPNG_broken_url("File")
def test_WebPageConversionFromSVGToPNG_broken_url(self):
""" Test Convert one SVG Image with an broken image href
"""
self._testImageConversionFromSVGToPNG_broken_url("Web Page")
def test_ImageConversionFromSVGToPNG_empty_file(self):
""" Test Convert one SVG Image with an empty svg
"""
self._testImageConversionFromSVGToPNG_empty_file("Image")
def test_FileConversionFromSVGToPNG_empty_file(self):
""" Test Convert one SVG Image with an empty svg
"""
self._testImageConversionFromSVGToPNG_empty_file("File")
def test_WebPageConversionFromSVGToPNG_empty_file(self):
""" Test Convert one SVG Image with an empty svg
"""
self._testImageConversionFromSVGToPNG_empty_file("Web Page")
def test_ImageConversionFromSVGToPNG_file_url(self):
""" Test Convert one SVG Image with an image using local path (file)
at the url of the image tag. ie:
......@@ -918,8 +1012,6 @@ return True
self._testImageConversionFromSVGToPNG(
"Web Page", "user-TESTSVG-CASE-FULLURL")
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestERP5WebWithDms))
......
......@@ -54,7 +54,7 @@
y="0.99838382"
x="0.11206181"
id="image3065"
xlink:href="file://REPLACE_THE_URL_HERE"
xlink:href="REPLACE_THE_URL_HERE"
height="369.32098"
width="523.56" />
<rect
......
#############################################################################
#
# Copyright (c) 2011 Nexedi SA and Contributors. All Rights Reserved.
# Rafael Monnerat <rafael@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import urllib2
from lxml import etree
from Products.ERP5.Document.Document import ConversionError
def getDataURI(url):
try:
data = urllib2.urlopen(url)
except Exception, e:
raise ConversionError("Error to transform url (%s) into data uri. ERROR = %s" % (url, Exception(e)))
return 'data:%s;base64,%s' % (data.info()["content-type"],
data.read().encode("base64").replace('\n', ""))
def transformUrlToDataURI(content):
if content is None or len(content) == 0:
return content
root = etree.fromstring(content)
# Prevent namespace contains "None" included into svg by mistake
namespace_dict = root.nsmap.copy()
namespace_dict.pop(None, "discard")
# Get all images which uses xlink:href
image_list = root.xpath("//svg:image[@xlink:href]", namespaces=namespace_dict)
xlink_href = "{%s}href" % namespace_dict.get("xlink", None)
# Transform all images which uses url, into data URI
for image in image_list:
url_value = image.get(xlink_href)
if url_value.startswith("http"):
image.set(xlink_href, getDataURI(image.get(xlink_href)))
return """<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n""" + \
etree.tostring(root)
......@@ -72,6 +72,7 @@ from Products.ERP5Type.patches import OFSItem
from Products.ERP5Type.patches import ExternalMethod
from Products.ERP5Type.patches import User
from Products.ERP5Type.patches import zopecontenttype
from Products.ERP5Type.patches import OFSImage
# These symbols are required for backward compatibility
from Products.ERP5Type.patches.PropertyManager import ERP5PropertyManager
......
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
Monkey-patch to fix OFS.Image, which is not capable to detect the SVG
Content Type format.
Monkey patch uses 2.12.3 original code.
"""
import OFS.Image
import struct
from cStringIO import StringIO
def getImageInfo_with_svg_fix(data):
data = str(data)
size = len(data)
height = -1
width = -1
content_type = ''
# handle GIFs
if (size >= 10) and data[:6] in ('GIF87a', 'GIF89a'):
# Check to see if content_type is correct
content_type = 'image/gif'
w, h = struct.unpack("<HH", data[6:10])
width = int(w)
height = int(h)
# See PNG v1.2 spec (http://www.cdrom.com/pub/png/spec/)
# Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
# and finally the 4-byte width, height
elif ((size >= 24) and (data[:8] == '\211PNG\r\n\032\n')
and (data[12:16] == 'IHDR')):
content_type = 'image/png'
w, h = struct.unpack(">LL", data[16:24])
width = int(w)
height = int(h)
# Maybe this is for an older PNG version.
elif (size >= 16) and (data[:8] == '\211PNG\r\n\032\n'):
# Check to see if we have the right content type
content_type = 'image/png'
w, h = struct.unpack(">LL", data[8:16])
width = int(w)
height = int(h)
# handle JPEGs
elif (size >= 2) and (data[:2] == '\377\330'):
content_type = 'image/jpeg'
jpeg = StringIO(data)
jpeg.read(2)
b = jpeg.read(1)
try:
while (b and ord(b) != 0xDA):
while (ord(b) != 0xFF): b = jpeg.read(1)
while (ord(b) == 0xFF): b = jpeg.read(1)
if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
jpeg.read(3)
h, w = struct.unpack(">HH", jpeg.read(4))
break
else:
jpeg.read(int(struct.unpack(">H", jpeg.read(2))[0])-2)
b = jpeg.read(1)
width = int(w)
height = int(h)
except: pass
# MONKEY PATCH START HERE
# Handle SVG
elif ("</svg>" in data):
content_type = 'image/svg+xml'
# MONKEY PATCH ENDS HERE
return content_type, width, height
OFS.Image.getImageInfo = getImageInfo_with_svg_fix
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