Commit b65fc696 authored by Jérome Perrin's avatar Jérome Perrin Committed by Aurel

Image: fallback to PIL to guess images content and size

This fixes problem that some formats such as tiff were not supported.
parent 8141b89d
...@@ -31,7 +31,6 @@ ...@@ -31,7 +31,6 @@
############################################################################## ##############################################################################
import os import os
import struct
import subprocess import subprocess
from cStringIO import StringIO from cStringIO import StringIO
...@@ -45,9 +44,10 @@ from erp5.component.document.File import File ...@@ -45,9 +44,10 @@ from erp5.component.document.File import File
from erp5.component.document.Document import Document, ConversionError,\ from erp5.component.document.Document import Document, ConversionError,\
VALID_TEXT_FORMAT_LIST, VALID_TRANSPARENT_IMAGE_FORMAT_LIST,\ VALID_TEXT_FORMAT_LIST, VALID_TRANSPARENT_IMAGE_FORMAT_LIST,\
DEFAULT_DISPLAY_ID_LIST, _MARKER DEFAULT_DISPLAY_ID_LIST, _MARKER
from os.path import splitext
from OFS.Image import Image as OFSImage from OFS.Image import Image as OFSImage
from OFS.Image import getImageInfo from OFS.Image import getImageInfo
import PIL.Image
from zLOG import LOG, WARNING from zLOG import LOG, WARNING
from erp5.component.module.ImageUtil import transformUrlToDataURI from erp5.component.module.ImageUtil import transformUrlToDataURI
...@@ -111,23 +111,26 @@ class Image(TextConvertableMixin, File, OFSImage): ...@@ -111,23 +111,26 @@ class Image(TextConvertableMixin, File, OFSImage):
def _update_image_info(self): def _update_image_info(self):
""" """
This method tries to determine the content type of an image and This method tries to determine the content type of an image and
its geometry. It uses currently OFS.Image for this purpose. its geometry.
However, this method is known to be too simplistic.
TODO:
- use image magick or PIL
""" """
self.size = len(self.data) self.size = len(self.data)
content_type, width, height = getImageInfo(self.data) content_type, width, height = getImageInfo(self.data)
if not content_type: if not content_type:
if self.size >= 30 and self.data[:2] == 'BM': try:
header = struct.unpack('<III', self.data[14:26]) image = PIL.Image.open(StringIO(str(self.data)))
if header[0] >= 12: except IOError:
content_type = 'image/x-bmp' width = height = -1
width, height = header[1:] content_type = 'application/unknown'
else:
width, height = image.size
content_type = image.get_format_mimetype()
# normalize the mimetype using the registry
mimetype_list = self.getPortalObject().mimetypes_registry.lookup(content_type)
if mimetype_list:
content_type = mimetype_list[0].normalized()
self.height = height self.height = height
self.width = width self.width = width
self._setContentType(content_type or 'application/unknown') self._setContentType(content_type)
def _upgradeImage(self): def _upgradeImage(self):
""" """
...@@ -303,8 +306,14 @@ class Image(TextConvertableMixin, File, OFSImage): ...@@ -303,8 +306,14 @@ class Image(TextConvertableMixin, File, OFSImage):
kw['image_size'] = image_size kw['image_size'] = image_size
display = kw.pop('display', None) display = kw.pop('display', None)
crop = kw.pop('crop', None) crop = kw.pop('crop', None)
mime, image = self._makeDisplayPhoto(crop=crop, **kw) mime, image_data = self._getContentTypeAndImageData(
image_data = image.data format=format,
quality=quality,
resolution=kw.get('resolution'),
frame=kw.get('frame'),
image_size=image_size,
crop=crop,
)
# as image will always be requested through a display not by passing exact # as image will always be requested through a display not by passing exact
# pixels we need to restore this way in cache # pixels we need to restore this way in cache
if display is not None: if display is not None:
...@@ -395,7 +404,7 @@ class Image(TextConvertableMixin, File, OFSImage): ...@@ -395,7 +404,7 @@ class Image(TextConvertableMixin, File, OFSImage):
return StringIO(image) return StringIO(image)
raise ConversionError('Image conversion failed (%s).' % err) raise ConversionError('Image conversion failed (%s).' % err)
def _getDisplayData( def _getContentTypeAndImageData(
self, self,
format, # pylint: disable=redefined-builtin format, # pylint: disable=redefined-builtin
quality, quality,
...@@ -404,7 +413,7 @@ class Image(TextConvertableMixin, File, OFSImage): ...@@ -404,7 +413,7 @@ class Image(TextConvertableMixin, File, OFSImage):
image_size, image_size,
crop, crop,
): ):
"""Return raw photo data for given display.""" """Return the content type and the image data as str or PData."""
if crop: if crop:
width, height = image_size width, height = image_size
else: else:
...@@ -413,29 +422,23 @@ class Image(TextConvertableMixin, File, OFSImage): ...@@ -413,29 +422,23 @@ class Image(TextConvertableMixin, File, OFSImage):
and quality == self.getDefaultImageQuality(format) and resolution is None and frame is None\ and quality == self.getDefaultImageQuality(format) and resolution is None and frame is None\
and not format: and not format:
# No resizing, no conversion, return raw image # No resizing, no conversion, return raw image
return self.getData() return self.getContentType(), self.getData()
return self._resize(quality, width, height, format, resolution, frame, crop) image_file = self._resize(quality, width, height, format, resolution, frame, crop)
image = OFSImage('', '', image_file)
def _makeDisplayPhoto( content_type = image.content_type
self, if content_type == 'application/octet-stream':
format=None, # pylint: disable=redefined-builtin # If OFS Image could not guess content type, try with PIL
quality=_MARKER, image_file.seek(0)
resolution=None, try:
frame=None, pil_image = PIL.Image.open(image_file)
image_size=None, except IOError:
crop=False, pass
): else:
"""Create given display.""" content_type = pil_image.get_format_mimetype()
if quality is _MARKER: mimetype_list = self.getPortalObject().mimetypes_registry.lookup(content_type)
quality = self.getDefaultImageQuality(format) if mimetype_list:
width, height = image_size # pylint: disable=unpacking-non-sequence content_type = mimetype_list[0].normalized()
base, ext = splitext(self.id) return content_type, image.data
id_ = '%s_%s_%s.%s'% (base, width, height, ext,)
image = OFSImage(id_, self.getTitle(),
self._getDisplayData(format, quality, resolution,
frame, image_size,
crop))
return image.content_type, aq_base(image)
def _getAspectRatioSize(self, width, height): def _getAspectRatioSize(self, width, height):
"""Return proportional dimensions within desired size.""" """Return proportional dimensions within desired size."""
...@@ -455,10 +458,6 @@ class Image(TextConvertableMixin, File, OFSImage): ...@@ -455,10 +458,6 @@ class Image(TextConvertableMixin, File, OFSImage):
width = img_width * height / img_height width = img_width * height / img_height
return (width, height) return (width, height)
def _validImage(self):
"""At least see if it *might* be valid."""
return self.getWidth() and self.getHeight() and self.getData() and self.getContentType()
security.declareProtected(Permissions.AccessContentsInformation, 'getSizeFromImageDisplay') security.declareProtected(Permissions.AccessContentsInformation, 'getSizeFromImageDisplay')
def getSizeFromImageDisplay(self, image_display): def getSizeFromImageDisplay(self, image_display):
"""Return the size for this image display, """Return the size for this image display,
......
...@@ -73,12 +73,6 @@ class TestERP5Base(ERP5TypeTestCase): ...@@ -73,12 +73,6 @@ class TestERP5Base(ERP5TypeTestCase):
## Usefull methods ## Usefull methods
################################## ##################################
def makeImageFileUpload(self, filename):
import Products.ERP5.tests
return FileUpload(
os.path.join(os.path.dirname(Products.ERP5.tests.__file__),
'test_data', 'images', filename))
def login_as_auditor(self): def login_as_auditor(self):
"""Create a new member user with Auditor role, and login """Create a new member user with Auditor role, and login
""" """
...@@ -936,52 +930,6 @@ class TestERP5Base(ERP5TypeTestCase): ...@@ -936,52 +930,6 @@ class TestERP5Base(ERP5TypeTestCase):
bank_account.setBankCountryCode('bank-country-code') bank_account.setBankCountryCode('bank-country-code')
self.assertEqual(bank_account.getReference(), 'iban') self.assertEqual(bank_account.getReference(), 'iban')
def test_CreateImage(self):
# We can add Images inside Persons and Organisation
for entity in (self.getPersonModule().newContent(portal_type='Person'),
self.getOrganisationModule().newContent(portal_type='Organisation')):
image = entity.newContent(portal_type='Embedded File')
self.assertEqual([], image.checkConsistency())
image.view() # viewing the image does not cause error
def test_ConvertImage(self):
image = self.portal.newContent(portal_type='Image', id='test_image')
image.edit(file=self.makeImageFileUpload('erp5_logo.png'))
self.assertEqual('image/png', image.getContentType())
self.assertEqual((320, 250), (image.getWidth(), image.getHeight()))
def convert(**kw):
image_type, image_data = image.convert('jpg', display='thumbnail', **kw)
self.assertEqual('image/jpeg', image_type)
thumbnail = self.portal.newContent(temp_object=True, portal_type='Image',
id='thumbnail', data=image_data)
self.assertEqual(image_type, thumbnail.getContentType())
self.assertEqual((128, 100), (thumbnail.getWidth(),
thumbnail.getHeight()))
return thumbnail.getSize()
self.assertTrue(convert() < convert(quality=100))
def test_ConvertImagePdata(self):
image = self.portal.newContent(portal_type='Image', id='test_image')
image.edit(file=self.makeImageFileUpload('erp5_logo.bmp'))
from OFS.Image import Pdata
self.assertTrue(isinstance(image.data, Pdata))
image_type, image_data = image.convert('jpg', display='thumbnail')
self.assertEqual('image/jpeg', image_type)
# magic
self.assertEqual('\xff', image_data[0])
self.assertEqual('\xd8', image_data[1])
def test_ImageSize(self):
image = self.portal.newContent(portal_type='Image', id='test_image')
image.edit(file=self.makeImageFileUpload('erp5_logo.png'))
self.assertEqual(320, image.getWidth())
self.assertEqual(250, image.getHeight())
image.edit(file=self.makeImageFileUpload('erp5_logo_small.png'))
self.assertEqual(160, image.getWidth())
self.assertEqual(125, image.getHeight())
def test_Person_getCareerStartDate(self): def test_Person_getCareerStartDate(self):
# Person_getCareerStartDate scripts returns the date when an employee # Person_getCareerStartDate scripts returns the date when an employee
# started to work for an employer # started to work for an employer
...@@ -1961,3 +1909,110 @@ class Base_getDialogSectionCategoryItemListTest(ERP5TypeTestCase): ...@@ -1961,3 +1909,110 @@ class Base_getDialogSectionCategoryItemListTest(ERP5TypeTestCase):
], ],
['Another Top Level Group', 'group/main_group_2'], ['Another Top Level Group', 'group/main_group_2'],
]) ])
class TestImage(ERP5TypeTestCase):
"""Tests for images support.
"""
def makeImageFileUpload(self, filename):
import Products.ERP5.tests
return FileUpload(
os.path.join(os.path.dirname(Products.ERP5.tests.__file__),
'test_data', 'images', filename))
def test_CreateImage(self):
# We can add Images inside Persons and Organisation
for entity in (self.getPersonModule().newContent(portal_type='Person'),
self.getOrganisationModule().newContent(portal_type='Organisation')):
image = entity.newContent(portal_type='Embedded File')
self.assertEqual([], image.checkConsistency())
image.view() # viewing the image does not cause error
def test_ConvertImage(self):
image = self.portal.newContent(portal_type='Image', id='test_image')
image.edit(file=self.makeImageFileUpload('erp5_logo.png'))
self.assertEqual('image/png', image.getContentType())
self.assertEqual((320, 250), (image.getWidth(), image.getHeight()))
def convert(**kw):
image_type, image_data = image.convert('jpg', display='thumbnail', **kw)
self.assertEqual('image/jpeg', image_type)
thumbnail = self.portal.newContent(temp_object=True, portal_type='Image',
id='thumbnail', data=image_data)
self.assertEqual(image_type, thumbnail.getContentType())
self.assertEqual((128, 100), (thumbnail.getWidth(),
thumbnail.getHeight()))
return thumbnail.getSize()
self.assertTrue(convert() < convert(quality=100))
def test_ConvertImagePdata(self):
image = self.portal.newContent(portal_type='Image', id='test_image')
image.edit(file=self.makeImageFileUpload('erp5_logo.bmp'))
from OFS.Image import Pdata
self.assertTrue(isinstance(image.data, Pdata))
image_type, image_data = image.convert('jpg', display='thumbnail')
self.assertEqual('image/jpeg', image_type)
# magic
self.assertEqual('\xff', image_data[0])
self.assertEqual('\xd8', image_data[1])
def test_ImageSize(self):
for filename, size in (
('erp5_logo.png', (320, 250)),
('erp5_logo_small.png', (160, 125)),
('erp5_logo.jpg', (320, 250)),
('erp5_logo.bmp', (320, 250)),
('erp5_logo.gif', (320, 250)),
('erp5_logo.tif', (320, 250)),
('empty.png', (0, 0)),
('broken.png', (-1, -1)),
('../broken_html.html', (-1, -1)),
):
image = self.portal.newContent(portal_type='Image', id=self.id())
image.edit(file=self.makeImageFileUpload(filename))
self.assertEqual(
(image.getWidth(), image.getHeight()),
size,
(filename, (image.getWidth(), image.getHeight()), size))
self.portal.manage_delObjects([self.id()])
def test_ImageContentTypeFromData(self):
for filename, content_type in (
('erp5_logo.png', 'image/png'),
('erp5_logo_small.png', 'image/png'),
('erp5_logo.jpg', 'image/jpeg'),
('erp5_logo.bmp', 'image/x-ms-bmp'),
('erp5_logo.gif', 'image/gif'),
('erp5_logo.tif', 'image/tiff'),
('broken.png', 'application/unknown'),
('empty.png', 'application/unknown'),
('../broken_html.html', 'application/unknown'),
):
image = self.portal.newContent(portal_type='Image', id=self.id())
image.edit(data=self.makeImageFileUpload(filename).read())
self.assertEqual(
image.getContentType(),
content_type,
(filename, image.getContentType(), content_type))
self.portal.manage_delObjects([self.id()])
def test_ImageContentTypeFromFile(self):
# with file= argument the filename also play a role in the type detection
for filename, content_type in (
('erp5_logo.png', 'image/png'),
('erp5_logo_small.png', 'image/png'),
('erp5_logo.jpg', 'image/jpeg'),
('erp5_logo.bmp', 'image/x-ms-bmp'),
('erp5_logo.gif', 'image/gif'),
('erp5_logo.tif', 'image/tiff'),
('broken.png', 'image/png'),
('empty.png', 'application/unknown'),
):
image = self.portal.newContent(portal_type='Image', id=self.id())
image.edit(file=self.makeImageFileUpload(filename))
self.assertEqual(
image.getContentType(),
content_type,
(filename, image.getContentType(), content_type))
self.portal.manage_delObjects([self.id()])
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