From 341732a38c1fc549b184b7bda63d380c0965074f Mon Sep 17 00:00:00 2001 From: Arne Holzenburg Date: Sat, 22 Mar 2014 13:35:13 +0100 Subject: [PATCH 1/2] Transpose based on EXIF orientation Transpose the original image before resizing based on EXIF orientation information to display image in the correct orientation. If someone uploads an image from an iPhone (for example) the image is currently often rendered in the wrong orientation. --- avatar/models.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/avatar/models.py b/avatar/models.py index d243200..248e060 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -116,12 +116,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: From 631eac80bb78e9ef2914340d4f4bd6353f196797 Mon Sep 17 00:00:00 2001 From: Fabian Stehle Date: Tue, 11 Jul 2017 21:54:41 +0200 Subject: [PATCH 2/2] Test transposing thumbnails based on exif data --- tests/data/image_exif_orientation.jpg | Bin 0 -> 4718 bytes tests/data/image_no_exif.jpg | Bin 0 -> 4717 bytes tests/tests.py | 25 +++++++++++++++++++++++-- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 tests/data/image_exif_orientation.jpg create mode 100644 tests/data/image_no_exif.jpg diff --git a/tests/data/image_exif_orientation.jpg b/tests/data/image_exif_orientation.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2df1300a965db9667b972a75ef6d514648f6caff GIT binary patch literal 4718 zcmeHKYfw{16yBQ$uhzy0g0Gqo#8+#cAdn$uB!J>H9R;nnGnEO+1tKIjB%ujZje<(s zQ5kCmYl~uSmD;I}inUs+BKYWIl!u5=J0(~Z7^%Sr1`Kz*H$bS4%s6%U(J6Z;yC-|j zerLaL_s3ay4K{<(3*r;v0fqrE4}AdI!NoYMHWdJ+63heu_yBKg44@+m174_`0GMtY zfD~+^$6kR=?~kFO7!g1{V4)1LH!i70=YE%!H9E z;m-y@EEGjZg%MJbh%1^+8IchH&`YuYOlZrcG!LStls;%v*wF6#Al+;>NCg7DiLcTS zDL7wE=mb`kK_KD_1t3~(HK^1XxS5-Rr)l*v-rXNg^SD}#jJGsgDO4I1c)E6et`Sel zeRHuoH$xqz;mPN4qpeb_&Y;81Dy~(RsW(ZjGM*b;ifn3Hz~j0@%o#G?GUY<9f-vIT zaDEtH$V01*npA0GY<&M(^iRg?-=xK2;aenp!k8uyMMXskgkphMJPSq4GUe#aD(fu0 zDP#a47B{JlT7y|j=(!Z4Duu{4%Xlbr-rvHaLFE~M z9UyXHI$4X85kwOp|lb~c!bth_WJJfBCzVhnE#zJtI(YD5S`yFS@YIp{)eSijN z=olN&*chFS!79K(E-R?>-Q20I64~8Fgl&apfj0N8f+Ev0|uKp zCRjAr%YTu|o0kHV9>bn6GEp>n?7S^SorLS$SA2I zHZFeN{DcKbi{^^b{ z_I&x(-hE|X?>~H`{OEVbzCV8AY-QEC^A|4u^mA?9mHMj<*BYDbEjRwWdFys-TYKld z`wt#=J#u#UP`pqT-A4=ceaxQXg+#z;3&;+DTD!GkZQ_dH;%;hu9#m-+%%=WnQd71oC1CXf^yD zJw`^sCh#&HLzjup268}>)6s)vjURYwpu_5+T{T0yYKL~!4eh!zw5$Fh%ek_^6_8JE zmOvm1I%6ZVw~|&$uu%_`S{xMcVB6^vt#n6y^XfgwxUzE##42s+sl<% zS%OEm^HwI5WLY*uORAz3o5<=*5?05q1L`^7t~>iau|LZzV{Ih_N1Ds*j`H4}L8D@d z|0=88wRPvp`p~FfT;4qyPNHHpyLYPV4acL(>!0-|H$eVaKF~@Sc5L#|m6C zN^Wr+yGw2yiLPTm+*l_*-*UPstG>7RSXbDpos&yuhMj6ZSkJm?7`VqQc-6W5bme`r z*>kbwCkmY*HrF0omkELsKSJ=$A1&)``#JIGQb;CEguoI+9?fZk09~bW8<`|OpZxg3 luAX-5xZcQv*I9PQAa=trVpk6%cFizi*A63g-7sP|{te%o6NLZ( literal 0 HcmV?d00001 diff --git a/tests/data/image_no_exif.jpg b/tests/data/image_no_exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6491989f548f9a981a474b63c38501b01cf10bbe GIT binary patch literal 4717 zcmeHKc~BEq9R6(%F703d#j6s7TB}1YkU+)|3kV()MU-Nlj!upR0^~3WCQv1cTC8oQ zYPI$RYbzep;sLd_qbUVE+Np{tptVYnsyMWOSQ=8u>)U`(J2KO$!yldUcJ}SJ`}@7` zz3+QFJHJKsr~yVTh?^e=1OX6_4?uP}H`k(00g%aH1^{@0C*cQlj0k)HF&>z144@z; z^y745dS4C==Li7z;EPWKA#~#ajPvrBZ#bFRf6j$duGHb-K}%wZ?48&e^zW^Q-w=Ufa53=i9q>zq9Av_deeD$*22^ z4;(yn?09L}7bm{_>g2Z-l~rfXo;&~D<(ey3YwPN-UAH&hZu5bTE`nyJgtHjT$pR0*7nXR+SNn5>8@vLdx9>e# z?j0PO*fv^WsP-8%CComxos#BO_Roan{VQet!UlBx2r>9q=II*3Bu~eHN28zc&&V_A zI*gHc>6ul3oGo+t*pb^(sFo) z^H%4gqz&hV2po&g^CMG|^PQ)YcQ+idwbYNFR9F8>`pp_v;%DinKU`^Vv(I5d03;A| jf>5o?<^H7feOU%BbW}HZ=+fY!>cK;o2M^T@9=i4?!RaAm literal 0 HcmV?d00001 diff --git a/tests/tests.py b/tests/tests.py index 8ae0b98..5274e06 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 from django.core.urlresolvers import reverse @@ -10,7 +11,7 @@ from avatar.conf import settings from avatar.utils import get_primary_avatar, get_user_model from avatar.models import Avatar from avatar.templatetags import avatar_tags -from PIL import Image +from PIL import Image, ImageChops def upload_helper(o, filename): @@ -22,8 +23,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') @@ -172,6 +182,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))