Skip to content

Commit

Permalink
Restructured the Exif orientation handling.
Browse files Browse the repository at this point in the history
* Moved logic into a resize() util function that wraps
  PIL.Image.resize().
* Added and utilized a couple utility functions for getting
  image dimensions, taking orientation into account.
  • Loading branch information
harrislapiroff committed Mar 10, 2014
1 parent 920510c commit 9445ece
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 22 deletions.
19 changes: 11 additions & 8 deletions daguerre/adjustments.py
@@ -1,6 +1,8 @@
from __future__ import division
from six.moves import xrange

from daguerre.utils import exif_aware_resize, exif_aware_size

try:
from PIL import Image
except ImportError:
Expand Down Expand Up @@ -123,8 +125,8 @@ def calculate(self, dims, areas=None):
calculate.uses_areas = False

def adjust(self, image, areas=None):
image_width, image_height = image.size
new_width, new_height = self.calculate(image.size)
image_width, image_height = exif_aware_size(image)
new_width, new_height = self.calculate(exif_aware_size(image))

This comment has been minimized.

Copy link
@harrislapiroff

harrislapiroff Mar 10, 2014

Author Contributor

Probably should do self.calculate((image_width, image_height)), eh? No sense in running the function twice.


if (new_width, new_height) == (image_width, image_height):
return image.copy()
Expand All @@ -135,7 +137,8 @@ def adjust(self, image, areas=None):
f = Image.ANTIALIAS
else:
f = Image.BICUBIC
return image.resize((new_width, new_height), f)

return exif_aware_resize(image, (new_width, new_height), f)
adjust.uses_areas = False


Expand Down Expand Up @@ -163,8 +166,8 @@ def calculate(self, dims, areas=None):
calculate.uses_areas = False

def adjust(self, image, areas=None):
image_width, image_height = image.size
new_width, new_height = self.calculate(image.size)
image_width, image_height = exif_aware_size(image)
new_width, new_height = self.calculate(exif_aware_size(image))

This comment has been minimized.

Copy link
@harrislapiroff

harrislapiroff Mar 10, 2014

Author Contributor

Ditto.


if (new_width, new_height) == (image_width, image_height):
return image.copy()
Expand Down Expand Up @@ -275,7 +278,7 @@ def calculate(self, dims, areas=None):
return area.width, area.height

def adjust(self, image, areas=None):
image_width, image_height = image.size
image_width, image_height = exif_aware_size(image)

if not areas:
return image.copy()
Expand Down Expand Up @@ -332,8 +335,8 @@ def calculate(self, dims, areas=None):
calculate.uses_areas = False

def adjust(self, image, areas=None):
image_width, image_height = image.size
new_width, new_height = self.calculate(image.size)
image_width, image_height = exif_aware_size(image)
new_width, new_height = self.calculate(exif_aware_size(image))

This comment has been minimized.

Copy link
@harrislapiroff

harrislapiroff Mar 10, 2014

Author Contributor

Ditto.


if (new_width, new_height) == (image_width, image_height):
return image.copy()
Expand Down
7 changes: 2 additions & 5 deletions daguerre/helpers.py
Expand Up @@ -3,7 +3,7 @@
import ssl

from django.conf import settings
from django.core.files.images import ImageFile, get_image_dimensions
from django.core.files.images import ImageFile
from django.core.files.storage import default_storage
from django.core.urlresolvers import reverse
from django.http import QueryDict
Expand All @@ -18,7 +18,7 @@

from daguerre.adjustments import registry
from daguerre.models import Area, AdjustedImage
from daguerre.utils import make_hash, save_image, KEEP_FORMATS, DEFAULT_FORMAT, apply_exif_orientation
from daguerre.utils import make_hash, save_image, get_image_dimensions, KEEP_FORMATS, DEFAULT_FORMAT

# If any of the following errors appear during file manipulations, we will
# treat them as IOErrors.
Expand Down Expand Up @@ -289,9 +289,6 @@ def _adjust(self, storage_path):
else:
areas = None

# Before doing any adjustments, apply Exif orientation.
im = apply_exif_orientation(im)

for adjustment in self.adjustments:
im = adjustment.adjust(im, areas=areas)

Expand Down
110 changes: 101 additions & 9 deletions daguerre/utils.py
@@ -1,3 +1,5 @@
import zlib

from hashlib import sha1

from django.core.files.base import File
Expand Down Expand Up @@ -25,6 +27,8 @@
7: (Image.ROTATE_90, Image.FLIP_LEFT_RIGHT,),
8: (Image.ROTATE_90,),
}
#: Which Exif orientation tags correspond to a 90deg or 270deg rotation.
ROTATION_TAGS = (5, 6, 7, 8)
#: Map human-readable Exif tag names to their markers.
EXIF_TAGS = dict((v,k) for k, v in ExifTags.TAGS.items())

Expand All @@ -38,6 +42,21 @@ def make_hash(*args, **kwargs):
)).hexdigest()[start:stop:step]


def get_exif_orientation(image):
# Extract the orientation tag
try:
exif_data = image._getexif() # should be careful with that _method
except AttributeError:
# No Exif data, return None
return None
if exif_data is not None and EXIF_TAGS['Orientation'] in exif_data:
orientation = exif_data[EXIF_TAGS['Orientation']]
return orientation
# No Exif orientation tag, return None
return None



This comment has been minimized.

Copy link
@harrislapiroff

harrislapiroff Mar 10, 2014

Author Contributor

Overzealous linebreaking.

def apply_exif_orientation(image):
"""
Reads an image Exif data for orientation information. Applies the
Expand All @@ -48,22 +67,95 @@ def apply_exif_orientation(image):
Accepts a PIL image and returns a PIL image.
"""
# Extract the orientation tag
try:
exif_data = image._getexif() # should be careful with that _method
except AttributeError:
# No exif data, return original image
return image
if exif_data is not None and EXIF_TAGS['Orientation'] in exif_data:
orientation = exif_data[EXIF_TAGS['Orientation']]
# Apply the corresponding tranpositions
orientation = get_exif_orientation(image)
if orientation is not None:
# Apply corresponding transpositions
transpositions = ORIENTATION_TO_TRANSPOSE[orientation]
if transpositions:
for t in transpositions:
image = image.transpose(t)
return image


def exif_aware_size(image):
"""
Intelligently get an image size, flipping width and height if the Exif
orientation tag includes a 90deg or 270deg rotation.
:param image: A PIL Image.
:returns: A 2-tuple (width, height).
"""
# Extract the orientation tag
orientation = get_exif_orientation(image)
if orientation in ROTATION_TAGS:
# Exif data indicates image should be rotated. Flip dimensions.
return image.size[::-1]
return image.size


def exif_aware_resize(image, *args, **kwargs):
"""
Intelligently resize an image, taking Exif orientation into account. Takes
the same arguments as the PIL Image ``.resize()`` method.
:param image: A PIL Image.
:returns: An PIL Image object.
"""

image = apply_exif_orientation(image)
return image.resize(*args, **kwargs)


def get_image_dimensions(file_or_path, close=False):
"""
A modified version of ``django.core.files.images.get_image_dimensions``
which accounts for Exif orientation.
"""

from django.utils.image import ImageFile as PILImageFile

p = PILImageFile.Parser()
if hasattr(file_or_path, 'read'):
file = file_or_path
file_pos = file.tell()
file.seek(0)
else:
file = open(file_or_path, 'rb')
close = True
try:
# Most of the time PIL only needs a small chunk to parse the image and
# get the dimensions, but with some TIFF files PIL needs to parse the
# whole file.
chunk_size = 1024
while 1:
data = file.read(chunk_size)
if not data:
break
try:
p.feed(data)
except zlib.error as e:
# ignore zlib complaining on truncated stream, just feed more
# data to parser (ticket #19457).
if e.args[0].startswith("Error -5"):
pass
else:
raise
if p.image:
return exif_aware_size(p.image)
chunk_size *= 2
return None
finally:
if close:
file.close()
else:
file.seek(file_pos)


def save_image(
image,
storage_path,
Expand Down

0 comments on commit 9445ece

Please sign in to comment.