Projet

Général

Profil

0001-misc-automatically-resize-profile-image-27644.patch

Frédéric Péters, 29 octobre 2018 15:56

Télécharger (7,11 ko)

Voir les différences:

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(-)
src/authentic2/app_settings.py
145 145
    A2_OPENED_SESSION_COOKIE_NAME=Setting(default='A2_OPENED_SESSION', definition='Authentic session open'),
146 146
    A2_OPENED_SESSION_COOKIE_DOMAIN=Setting(default=None),
147 147
    A2_ATTRIBUTE_KINDS=Setting(default=(), definition='List of other attribute kinds'),
148
    A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE=Setting(default=200, definition='Max width and height for a profile image'),
148
    A2_ATTRIBUTE_KIND_PROFILE_IMAGE_SIZE=Setting(default=200, definition='Width and height for a profile image'),
149 149
    A2_VALIDATE_EMAIL=Setting(default=False, definition='Validate user email server by doing an RCPT command'),
150 150
    A2_VALIDATE_EMAIL_DOMAIN=Setting(default=True, definition='Validate user email domain'),
151 151
    A2_PASSWORD_POLICY_MIN_CLASSES=Setting(default=3, definition='Minimum number of characters classes to be present in passwords'),
src/authentic2/forms/fields.py
46 46
class ProfileImageField(FileField):
47 47
    widget = ProfileImageInput
48 48

  
49
    def __init__(self, *args, **kwargs):
50
        kwargs.setdefault(
51
            'help_text',
52
            _('Image must be JPG or PNG of size less '
53
              'than {max_size}x{max_size} pixels').format(max_size=self.max_size))
54
        super(ProfileImageField, self).__init__(*args, **kwargs)
55

  
56 49
    @property
57
    def max_size(self):
58
        return app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE
50
    def image_size(self):
51
        return app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_SIZE
59 52

  
60 53
    def clean(self, data, initial=None):
61 54
        if data is FILE_INPUT_CONTRADICTION or data is False or data is None:
......
66 59
                image = PIL.Image.open(io.BytesIO(data.read()))
67 60
        except (IOError, PIL.Image.DecompressionBombWarning):
68 61
            raise ValidationError(_('The image is not valid'))
69
        width, height = image.size
70
        max_size = app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE
71
        if width > max_size or height > max_size:
72
            raise ValidationError(_('The image is bigger than {max_size}x{max_size} pixels')
73
                                  .format(max_size=self.max_size))
62
        image = self.normalize_image(image)
74 63
        new_data = self.file_from_image(image, data.name)
75 64
        return super(ProfileImageField, self).clean(new_data, initial=initial)
76 65

  
......
85 74
            optimize=1)
86 75
        output.seek(0)
87 76
        return File(output, name=name)
77

  
78
    def normalize_image(self, image):
79
        width = height = self.image_size
80
        if abs((1.0 * width / height) - (1.0 * image.size[0] / image.size[1])) > 0.1:
81
            # aspect ratio change, crop the image first
82
            box = [0, 0, image.size[0], int(image.size[0] * (1.0 * height / width))]
83

  
84
            if box[2] > image.size[0]:
85
                box = [int(t * (1.0 * image.size[0] / box[2])) for t in box]
86
            if box[3] > image.size[1]:
87
                box = [int(t * (1.0 * image.size[1] / box[3])) for t in box]
88

  
89
            if image.size[0] > image.size[1]:  # landscape
90
                box[0] = (image.size[0] - box[2]) / 2  # keep the middle
91
                box[2] += box[0]
92
            else:
93
                box[1] = (image.size[1] - box[3]) / 4  # keep mostly the top
94
                box[3] += box[1]
95

  
96
            image = image.crop(box)
97
        return image.resize([width, height], PIL.Image.ANTIALIAS)
tests/test_attribute_kinds.py
1 1
# -*- coding: utf-8 -*-
2 2
import datetime
3
import os
4

  
5
import PIL.Image
6

  
7
from django.conf import settings
3 8

  
4 9
from authentic2.custom_user.models import User
5 10
from authentic2.models import Attribute
......
398 403
    response = form.submit()
399 404
    assert response.pyquery.find('.form-field-error #id_cityscape_image')
400 405

  
401
    # verify 201x201 image is refused
402
    form = response.form
403
    form.set('cityscape_image', Upload('tests/201x201.jpg'))
404
    form.set('password1', '12345abcdA')
405
    form.set('password2', '12345abcdA')
406
    response = form.submit()
407
    assert response.pyquery.find('.form-field-error #id_cityscape_image')
408

  
409 406
    # verify 200x200 image is accepted
410 407
    form = response.form
411 408
    form.set('cityscape_image', Upload('tests/200x200.jpg'))
......
413 410
    form.set('password2', '12345abcdA')
414 411
    response = form.submit()
415 412
    assert john().attributes.cityscape_image
413
    profile_filename = john().attributes.cityscape_image.name
416 414

  
417 415
    # verify API serves absolute URL for profile images
418 416
    app.authorization = ('Basic', (admin.username, admin.username))
......
429 427
    response = form.submit()
430 428
    assert john().attributes.cityscape_image == None
431 429

  
432
    # verify API serves absolute URL for profile images
430
    # verify API serves None for empty profile images
433 431
    app.authorization = ('Basic', (admin.username, admin.username))
434 432
    response = app.get('/api/users/%s/' % john().uuid)
435 433
    assert response.json['cityscape_image'] is None
434

  
435
    # verify 201x201 image is accepted and resized
436
    response = app.get('/accounts/edit/')
437
    form = response.form
438
    form.set('edit-profile-cityscape_image', Upload('tests/201x201.jpg'))
439
    response = form.submit()
440
    image = PIL.Image.open(open(os.path.join(settings.MEDIA_ROOT, john().attributes.cityscape_image.name)))
441
    assert image.width == 200
442
    assert image.height == 200
443
    assert john().attributes.cityscape_image.name != profile_filename
436
-