diff --git a/avatar/models.py b/avatar/models.py index 5268983..b97c0f3 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -127,12 +127,38 @@ class Avatar(models.Model): def thumbnail_exists(self, size): return self.avatar.storage.exists(self.avatar_name(size)) + def transpose_image(self, image): + """ + Transpose based on EXIF information. + Borrowed from django-imagekit: + imagekit.processors.Transpose + """ + EXIF_ORIENTATION_STEPS = { + 1: [], + 2: ['FLIP_LEFT_RIGHT'], + 3: ['ROTATE_180'], + 4: ['FLIP_TOP_BOTTOM'], + 5: ['ROTATE_270', 'FLIP_LEFT_RIGHT'], + 6: ['ROTATE_270'], + 7: ['ROTATE_90', 'FLIP_LEFT_RIGHT'], + 8: ['ROTATE_90'], + } + try: + orientation = image._getexif()[0x0112] + ops = EXIF_ORIENTATION_STEPS[orientation] + except: + ops = [] + for method in ops: + image = image.transpose(getattr(Image, method)) + return image + def create_thumbnail(self, size, quality=None): # invalidate the cache of the thumbnail with the given size first invalidate_cache(self.user, size) try: orig = self.avatar.storage.open(self.avatar.name, 'rb') image = Image.open(orig) + image = self.transpose_image(image) quality = quality or settings.AVATAR_THUMB_QUALITY w, h = image.size if w != size or h != size: diff --git a/tests/data/image_exif_orientation.jpg b/tests/data/image_exif_orientation.jpg new file mode 100644 index 0000000..2df1300 Binary files /dev/null and b/tests/data/image_exif_orientation.jpg differ diff --git a/tests/data/image_no_exif.jpg b/tests/data/image_no_exif.jpg new file mode 100644 index 0000000..6491989 Binary files /dev/null and b/tests/data/image_no_exif.jpg differ diff --git a/tests/tests.py b/tests/tests.py index 1538888..4466247 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,5 +1,6 @@ import os.path +import math from django.contrib.admin.sites import AdminSite from django.test import TestCase try: @@ -15,7 +16,7 @@ from avatar.utils import get_primary_avatar, get_user_model from avatar.models import Avatar from avatar.templatetags import avatar_tags from avatar.signals import avatar_deleted -from PIL import Image +from PIL import Image, ImageChops class AssertSignal: @@ -43,8 +44,17 @@ def upload_helper(o, filename): return response -class AvatarTests(TestCase): +def root_mean_square_difference(image1, image2): + "Calculate the root-mean-square difference between two images" + diff = ImageChops.difference(image1, image2).convert('L') + h = diff.histogram() + sq = (value * (idx ** 2) for idx, value in enumerate(h)) + sum_of_squares = sum(sq) + rms = math.sqrt(sum_of_squares / float(image1.size[0] * image1.size[1])) + return rms + +class AvatarTests(TestCase): def setUp(self): self.testdatapath = os.path.join(os.path.dirname(__file__), "data") self.user = get_user_model().objects.create_user('test', 'lennon@thebeatles.com', 'testpassword') @@ -199,6 +209,17 @@ class AvatarTests(TestCase): image = Image.open(avatar.avatar.storage.open(avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), 'rb')) self.assertEqual(image.mode, 'RGB') + def test_thumbnail_transpose_based_on_exif(self): + upload_helper(self, "image_no_exif.jpg") + avatar = get_primary_avatar(self.user) + image_no_exif = Image.open(avatar.avatar.storage.open(avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), 'rb')) + + upload_helper(self, "image_exif_orientation.jpg") + avatar = get_primary_avatar(self.user) + image_with_exif = Image.open(avatar.avatar.storage.open(avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), 'rb')) + + self.assertLess(root_mean_square_difference(image_with_exif, image_no_exif), 1) + def test_has_avatar_False_if_no_avatar(self): self.assertFalse(avatar_tags.has_avatar(self.user))