Commit f084c646 authored by Jérome Perrin's avatar Jérome Perrin

Base: support more image formats

By relying on PIL after our monkey-patched OFS.Image.getImageInfo.

We keep this monkey-patch for now, because it adds supports to svg

See merge request nexedi/erp5!1426
parents 6dce55b0 9ac96204
Pipeline #15809 failed with stage
...@@ -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
...@@ -1960,3 +1908,110 @@ class Base_getDialogSectionCategoryItemListTest(ERP5TypeTestCase): ...@@ -1960,3 +1908,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()])
...@@ -1249,16 +1249,51 @@ class TestDocument(TestDocumentMixin): ...@@ -1249,16 +1249,51 @@ class TestDocument(TestDocumentMixin):
self.assert_('I use reference to look up TEST' in self.assert_('I use reference to look up TEST' in
document.SearchableText()) document.SearchableText())
def test_PDFToImage(self): def test_PDFToPng(self):
upload_file = makeFileUpload('REF-en-001.pdf') upload_file = makeFileUpload('REF-en-001.pdf')
document = self.portal.portal_contributions.newContent(file=upload_file) document = self.portal.portal_contributions.newContent(file=upload_file)
self.assertEqual('PDF', document.getPortalType()) self.assertEqual('PDF', document.getPortalType())
_, image_data = document.convert(format='png', mime, image_data = document.convert(format='png',
frame=0, frame=0,
display='thumbnail') display='thumbnail')
self.assertEqual(mime, 'image/png')
# it's a valid PNG # it's a valid PNG
self.assertEqual('PNG', image_data[1:4]) self.assertEqual(image_data[1:4], 'PNG')
def test_PDFToJpg(self):
upload_file = makeFileUpload('REF-en-001.pdf')
document = self.portal.portal_contributions.newContent(file=upload_file)
self.assertEqual('PDF', document.getPortalType())
mime, image_data = document.convert(format='jpg',
frame=0,
display='thumbnail')
self.assertEqual(mime, 'image/jpeg')
self.assertEqual(image_data[6:10], 'JFIF')
def test_PDFToGif(self):
upload_file = makeFileUpload('REF-en-001.pdf')
document = self.portal.portal_contributions.newContent(file=upload_file)
self.assertEqual('PDF', document.getPortalType())
mime, image_data = document.convert(format='gif',
frame=0,
display='thumbnail')
self.assertEqual(mime, 'image/gif')
self.assertEqual(image_data[0:4], 'GIF8')
def test_PDFToTiff(self):
upload_file = makeFileUpload('REF-en-001.pdf')
document = self.portal.portal_contributions.newContent(file=upload_file)
self.assertEqual('PDF', document.getPortalType())
mime, image_data = document.convert(format='tiff',
frame=0,
display='thumbnail')
self.assertEqual(mime, 'image/tiff')
self.assertIn(image_data[0:2], ('II', 'MM'))
def test_PDF_content_information(self): def test_PDF_content_information(self):
upload_file = makeFileUpload('REF-en-001.pdf') upload_file = makeFileUpload('REF-en-001.pdf')
...@@ -2935,12 +2970,13 @@ class TestDocumentPerformance(TestDocumentMixin): ...@@ -2935,12 +2970,13 @@ class TestDocumentPerformance(TestDocumentMixin):
"Conversion took %s seconds and it is not less them 100.0 seconds" % \ "Conversion took %s seconds and it is not less them 100.0 seconds" % \
req_time) req_time)
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestDocument)) suite.addTest(unittest.makeSuite(TestDocument))
suite.addTest(unittest.makeSuite(TestDocumentWithSecurity)) suite.addTest(unittest.makeSuite(TestDocumentWithSecurity))
suite.addTest(unittest.makeSuite(TestDocumentPerformance)) suite.addTest(unittest.makeSuite(TestDocumentPerformance))
# Run erp5_base's TestImage with dms installed (because dms has specific interactions)
from erp5.component.test.testERP5Base import TestImage
suite.addTest(unittest.makeSuite(TestImage))
return suite return suite
# vim: syntax=python shiftwidth=2
\ No newline at end of file
...@@ -2,3 +2,4 @@ erp5_full_text_mroonga_catalog ...@@ -2,3 +2,4 @@ erp5_full_text_mroonga_catalog
erp5_core_proxy_field_legacy erp5_core_proxy_field_legacy
erp5_ingestion_mysql_innodb_catalog erp5_ingestion_mysql_innodb_catalog
erp5_ingestion_test erp5_ingestion_test
erp5_core_test
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
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