From 83f880fadeb3a10d9d87ec6855053d12f9328ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Mon, 29 Oct 2018 15:35:10 +0100 Subject: [PATCH 1/3] misc: automatically resize profile image (#27644) --- src/authentic2/app_settings.py | 2 +- src/authentic2/forms/fields.py | 38 +++++++++++++++++++++------------ tests/201x201.jpg | Bin 330 -> 795 bytes tests/test_attribute_kinds.py | 26 ++++++++++++++-------- 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/authentic2/app_settings.py b/src/authentic2/app_settings.py index fdb64d7e..67ecb553 100644 --- a/src/authentic2/app_settings.py +++ b/src/authentic2/app_settings.py @@ -145,7 +145,7 @@ default_settings = dict( A2_OPENED_SESSION_COOKIE_NAME=Setting(default='A2_OPENED_SESSION', definition='Authentic session open'), A2_OPENED_SESSION_COOKIE_DOMAIN=Setting(default=None), A2_ATTRIBUTE_KINDS=Setting(default=(), definition='List of other attribute kinds'), - A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE=Setting(default=200, definition='Max width and height for a profile image'), + A2_ATTRIBUTE_KIND_PROFILE_IMAGE_SIZE=Setting(default=200, definition='Width and height for a profile image'), A2_VALIDATE_EMAIL=Setting(default=False, definition='Validate user email server by doing an RCPT command'), A2_VALIDATE_EMAIL_DOMAIN=Setting(default=True, definition='Validate user email domain'), A2_PASSWORD_POLICY_MIN_CLASSES=Setting(default=3, definition='Minimum number of characters classes to be present in passwords'), diff --git a/src/authentic2/forms/fields.py b/src/authentic2/forms/fields.py index 3be157a6..613e121f 100644 --- a/src/authentic2/forms/fields.py +++ b/src/authentic2/forms/fields.py @@ -46,16 +46,9 @@ class CheckPasswordField(CharField): class ProfileImageField(FileField): widget = ProfileImageInput - def __init__(self, *args, **kwargs): - kwargs.setdefault( - 'help_text', - _('Image must be JPG or PNG of size less ' - 'than {max_size}x{max_size} pixels').format(max_size=self.max_size)) - super(ProfileImageField, self).__init__(*args, **kwargs) - @property - def max_size(self): - return app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE + def image_size(self): + return app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_SIZE def clean(self, data, initial=None): if data is FILE_INPUT_CONTRADICTION or data is False or data is None: @@ -66,11 +59,7 @@ class ProfileImageField(FileField): image = PIL.Image.open(io.BytesIO(data.read())) except (IOError, PIL.Image.DecompressionBombWarning): raise ValidationError(_('The image is not valid')) - width, height = image.size - max_size = app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE - if width > max_size or height > max_size: - raise ValidationError(_('The image is bigger than {max_size}x{max_size} pixels') - .format(max_size=self.max_size)) + image = self.normalize_image(image) new_data = self.file_from_image(image, data.name) return super(ProfileImageField, self).clean(new_data, initial=initial) @@ -85,3 +74,24 @@ class ProfileImageField(FileField): optimize=1) output.seek(0) return File(output, name=name) + + def normalize_image(self, image): + width = height = self.image_size + if abs((1.0 * width / height) - (1.0 * image.size[0] / image.size[1])) > 0.1: + # aspect ratio change, crop the image first + box = [0, 0, image.size[0], int(image.size[0] * (1.0 * height / width))] + + if box[2] > image.size[0]: + box = [int(t * (1.0 * image.size[0] / box[2])) for t in box] + if box[3] > image.size[1]: + box = [int(t * (1.0 * image.size[1] / box[3])) for t in box] + + if image.size[0] > image.size[1]: # landscape + box[0] = (image.size[0] - box[2]) / 2 # keep the middle + box[2] += box[0] + else: + box[1] = (image.size[1] - box[3]) / 4 # keep mostly the top + box[3] += box[1] + + image = image.crop(box) + return image.resize([width, height], PIL.Image.ANTIALIAS) diff --git a/tests/201x201.jpg b/tests/201x201.jpg index f9e749735b0d483d5dd7d89b850b8cb6a8845d07..521972f88f263b38a03920011f0af494ca8f4373 100644 GIT binary patch literal 795 zcmeH@xe>xJ5JkT%BXrr`wKRFVWT!cdSM4$Wb&$o(v%r%%*;6Jy|d2w*hC-d;9SL3-4HSP*mf;-vZvfc z<)kjH1Sz;6_Dj+gY%yTaiczrAtkMljc`C+O(F>Ydh+XBe#>Hlyt9fE&?kA#Fa*t+g Rab91Jp|Be3-7zDiBJlz-=85tND85k!DdGH)y;O1aB$#9a9QIKKcMlog1|3?@^ c1c36W;QuWK4v;Q;hE)lZ6&Y7B+yB1_0F<^9=Kufz diff --git a/tests/test_attribute_kinds.py b/tests/test_attribute_kinds.py index bc7e6307..317ce691 100644 --- a/tests/test_attribute_kinds.py +++ b/tests/test_attribute_kinds.py @@ -1,5 +1,10 @@ # -*- coding: utf-8 -*- import datetime +import os + +import PIL.Image + +from django.conf import settings from authentic2.custom_user.models import User from authentic2.models import Attribute @@ -398,14 +403,6 @@ def test_profile_image(db, app, admin, mailoutbox, media): response = form.submit() assert response.pyquery.find('.form-field-error #id_cityscape_image') - # verify 201x201 image is refused - form = response.form - form.set('cityscape_image', Upload('tests/201x201.jpg')) - form.set('password1', '12345abcdA') - form.set('password2', '12345abcdA') - response = form.submit() - assert response.pyquery.find('.form-field-error #id_cityscape_image') - # verify 200x200 image is accepted form = response.form form.set('cityscape_image', Upload('tests/200x200.jpg')) @@ -413,6 +410,7 @@ def test_profile_image(db, app, admin, mailoutbox, media): form.set('password2', '12345abcdA') response = form.submit() assert john().attributes.cityscape_image + profile_filename = john().attributes.cityscape_image.name # verify API serves absolute URL for profile images app.authorization = ('Basic', (admin.username, admin.username)) @@ -429,7 +427,17 @@ def test_profile_image(db, app, admin, mailoutbox, media): response = form.submit() assert john().attributes.cityscape_image == None - # verify API serves absolute URL for profile images + # verify API serves None for empty profile images app.authorization = ('Basic', (admin.username, admin.username)) response = app.get('/api/users/%s/' % john().uuid) assert response.json['cityscape_image'] is None + + # verify 201x201 image is accepted and resized + response = app.get('/accounts/edit/') + form = response.form + form.set('edit-profile-cityscape_image', Upload('tests/201x201.jpg')) + response = form.submit() + image = PIL.Image.open(open(os.path.join(settings.MEDIA_ROOT, john().attributes.cityscape_image.name))) + assert image.width == 200 + assert image.height == 200 + assert john().attributes.cityscape_image.name != profile_filename -- 2.19.1