Skip to content

Commit

Permalink
Implemention of PLIP 1673: Automatic image rotation based on Exif data
Browse files Browse the repository at this point in the history
PLIP 1673: plone/Products.CMFPlone#1673
This PLIP implementation introduced a straight foreward image rotation based on Exif information for JPEG and TIFF Images stored in plone.namedfile Image fields.
The concept of Exif orientation is described in detail in the following blog: http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/

It requires an additional external library as dependency: piexif (package: https://pypi.python.org/pypi/piexif; docs: http://piexif.readthedocs.org/en/latest/; github: https://github.com/hMatoba/Piexif)
piexif, allows reading and writing of Exif information, which offers more powerful image handling than a read only library. piexif also works very smart together with Pillow.

This PLIP superseeds prior attemts to add Exif based image rotation to plone.namedfile: #12, #13, #14

For testing of orientation change, images where needed to include for test files.
This is not part of this implementation.
The following github repositories have several examples of Images for orientation testing:
* https://github.com/recurser/exif-orientation-examples (JPEG Images with orientation 1-8 in Landscape and Portrait orientation)
* https://github.com/ianare/exif-samples (lots of different JPEG and TIFF Images, even corrupted images)

Additional to the PLIP implementation this commit includes a restructuring of the package.
* Image meta data detection has been moved to a subfolder utils and splited into several files for each image type
* Moved functions to utils as those are helper methods, so that base functionallity is easier to read
* Added basic TIFF Image handling. Prior TIFF were handled as Files not Images.

Last but not least:
Moved package version from 4.1.x to 4.2.0 as it introduced new features, following semantic versioning.
  • Loading branch information
loechel committed Sep 21, 2016
1 parent 0fe3cb0 commit df59b59
Show file tree
Hide file tree
Showing 8 changed files with 471 additions and 161 deletions.
14 changes: 13 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Changelog
=========

4.1.1 (unreleased)
4.2.0 (unreleased)
------------------

Breaking changes:
Expand All @@ -11,10 +11,22 @@ Breaking changes:
New features:

- *add item here*
- Add automatic image rotation based on EXIF data for all images.
Based on piexif library and ideas of maartenkling and ezvirtual.
Choosen piexif as it allow read and write of exif data for future enhancements.
http://piexif.readthedocs.org/en/latest/
For Orientation examples and description see http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/ test data https://github.com/recurser/exif-orientation-examples
Additional Test Images with different MIME-Types (JPEG and TIFF) and possible problems: https://github.com/ianare/exif-samples.git
[loechel]

Bug fixes:

- *add item here*
- Added handler for Tiff Images in getImageInfo.
[loechel]
- Restructured packages.
Moved image meta data detection in an own subfolder
[loechel]


4.1 (2016-09-14)
Expand Down
119 changes: 37 additions & 82 deletions plone/namedfile/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,29 @@
# The implementations in this file are largely borrowed
# from zope.app.file and z3c.blobfile
# and are licensed under the ZPL.
from cStringIO import StringIO
from logging import getLogger
from persistent import Persistent
from plone.namedfile.interfaces import INamedBlobFile
from plone.namedfile.interfaces import INamedBlobImage
from plone.namedfile.interfaces import INamedFile
from plone.namedfile.interfaces import INamedImage
from plone.namedfile.interfaces import IStorage
from plone.namedfile.utils import get_contenttype
from plone.namedfile.utils import get_exif
from plone.namedfile.utils import getImageInfo
from plone.namedfile.utils import rotate_image
from ZODB.blob import Blob
from zope.component import getUtility
from zope.interface import implementer
from zope.schema.fieldproperty import FieldProperty

import struct
import piexif
import transaction


log = getLogger(__name__)


MAXCHUNKSIZE = 1 << 16
IMAGE_INFO_BYTES = 1024
MAX_INFO_BYTES = 1 << 16
Expand Down Expand Up @@ -267,13 +273,24 @@ class NamedImage(NamedFile):

def __init__(self, data='', contentType='', filename=None):
self.contentType, self._width, self._height = getImageInfo(data)
self.data = data
self.filename = filename
self._setData(data)

# Allow override of the image sniffer
if contentType:
self.contentType = contentType

exif_data = get_exif(data)
if exif_data is not None:
log.debug('Image contains Exif Informations. '
'Test for Image Orientation and Rotate if necessary.'
'Exif Data: %s', exif_data)
orientation = exif_data['0th'].get(piexif.ImageIFD.Orientation, 1)
if 1 < orientation <= 8:
self.data, self._width, self._height, self.exif = rotate_image(
self.data)
self.exif_data = exif_data

def _setData(self, data):
super(NamedImage, self)._setData(data)

Expand All @@ -288,81 +305,6 @@ def getImageSize(self):
data = property(NamedFile._getData, _setData)


def getImageInfo(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 2. Edition spec (http://www.w3.org/TR/PNG/)
# Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
# and finally the 4-byte width, height
elif (
(size >= 24) and data.startswith('\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.startswith('\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.startswith('\377\330'):
content_type = 'image/jpeg'
jpeg = StringIO(data)
jpeg.read(2)
b = jpeg.read(1)
try:
w = -1
h = -1
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 struct.error:
pass
except ValueError:
pass
except TypeError:
pass

# handle BMPs
elif (size >= 30) and data.startswith('BM'):
kind = struct.unpack('<H', data[14:16])[0]
if kind == 40: # Windows 3.x bitmap
content_type = 'image/x-ms-bmp'
width, height = struct.unpack('<LL', data[18:26])

return content_type, width, height


@implementer(INamedBlobFile)
class NamedBlobFile(Persistent):
"""A file stored in a ZODB BLOB, with a filename"""
Expand Down Expand Up @@ -397,6 +339,7 @@ def _setData(self, data):
# Search for a storable that is able to store the data
dottedName = '.'.join((data.__class__.__module__,
data.__class__.__name__))
log.debug('Storage selected for data: %s', dottedName)
storable = getUtility(IStorage, name=dottedName)
storable.store(data, self._blob)

Expand Down Expand Up @@ -430,17 +373,30 @@ class NamedBlobImage(NamedBlobFile):
"""

def __init__(self, data='', contentType='', filename=None):
super(NamedBlobImage, self).__init__(data, filename=filename)
super(NamedBlobImage, self).__init__(data,
contentType=contentType,
filename=filename)

# Allow override of the image sniffer
if contentType:
self.contentType = contentType
exif_data = get_exif(self.data)
if exif_data is not None:
log.debug('Image contains Exif Informations. '
'Test for Image Orientation and Rotate if necessary.'
'Exif Data: %s', exif_data)
orientation = exif_data['0th'].get(piexif.ImageIFD.Orientation, 1)
if 1 < orientation <= 8:
self.data, self._width, self._height, self.exif = rotate_image(
self.data)
else:
self.exif = exif_data

def _setData(self, data):
super(NamedBlobImage, self)._setData(data)
firstbytes = self.getFirstBytes()
res = getImageInfo(firstbytes)
if res == ('image/jpeg', -1, -1):
if res == ('image/jpeg', -1, -1) or res == ('image/tiff', -1, -1):
# header was longer than firstbytes
start = len(firstbytes)
length = max(0, MAX_INFO_BYTES - start)
Expand Down Expand Up @@ -468,6 +424,5 @@ def getImageSize(self):
if (self._width, self._height) != (-1, -1):
return (self._width, self._height)

res = getImageInfo(self.data)
contentType, self._width, self._height = res
contentType, self._width, self._height = getImageInfo(self.data)
return (self._width, self._height)
77 changes: 0 additions & 77 deletions plone/namedfile/utils.py

This file was deleted.

Loading

1 comment on commit df59b59

@jenkins-plone-org
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@loechel Jenkins CI reporting about code analysis
See the full report here: http://jenkins.plone.org/job/package-plone.namedfile/56/violations

plone/namedfile/scaling.py:173:1: C901 'DefaultImageScalingFactory.__call__' is too complex (13)
plone/namedfile/utils/__init__.py:176:1: C901 'rotate_image' is too complex (17)

Follow these instructions to reproduce it locally.

Please sign in to comment.