comparison gpp/bio/forms.py @ 265:1ba2c6bf6eb7

Closing #98. Animated GIFs were losing their transparency and animated properties when saved as avatars. Reworked the avatar save process to only run the avatar through PIL if it is too big. This preserves the original uploaded file if it is within the desired size settings. This may still mangle big animated gifs. If this becomes a problem, then maybe look into calling the PIL Image.resize() method directly. Moved the PIL image specific functions from bio.forms to a new module: core.image for better reusability in the future.
author Brian Neal <bgneal@gmail.com>
date Fri, 24 Sep 2010 02:12:09 +0000
parents 272d3a8c98e8
children 88b2b9cb8c1f
comparison
equal deleted inserted replaced
264:91c0902de04d 265:1ba2c6bf6eb7
1 """ 1 """
2 This file contains the forms used by the bio application. 2 This file contains the forms used by the bio application.
3 """ 3 """
4 from PIL import ImageFile
5 from PIL import Image
6
7 try: 4 try:
8 from cStringIO import StringIO 5 from cStringIO import StringIO
9 except: 6 except:
10 from StringIO import StringIO 7 from StringIO import StringIO
11 8
14 from django.core.files.base import ContentFile 11 from django.core.files.base import ContentFile
15 from django.contrib.auth.models import User 12 from django.contrib.auth.models import User
16 13
17 from bio.models import UserProfile 14 from bio.models import UserProfile
18 from core.widgets import AutoCompleteUserInput 15 from core.widgets import AutoCompleteUserInput
16 from core.image import parse_image, downscale_image_square
19 17
20 18
21 class EditUserForm(forms.ModelForm): 19 class EditUserForm(forms.ModelForm):
22 """Form for editing the fields of the User model.""" 20 """Form for editing the fields of the User model."""
23 email = forms.EmailField(label='Email', required=True) 21 email = forms.EmailField(label='Email', required=True)
52 js = settings.GPP_THIRD_PARTY_JS['markitup'] + \ 50 js = settings.GPP_THIRD_PARTY_JS['markitup'] + \
53 settings.GPP_THIRD_PARTY_JS['jquery-ui'] + \ 51 settings.GPP_THIRD_PARTY_JS['jquery-ui'] + \
54 ('js/bio.js', 'js/timezone.js') 52 ('js/bio.js', 'js/timezone.js')
55 53
56 54
57 def get_image(file):
58 """
59 Returns a PIL Image from the supplied file.
60 Throws ValidationError if the file does not parse as an image file.
61 """
62 parser = ImageFile.Parser()
63 for chunk in file.chunks():
64 parser.feed(chunk)
65 try:
66 image = parser.close()
67 return image
68 except IOError:
69 pass
70 raise forms.ValidationError("Upload a valid image. " +
71 "The file you uploaded was either not an image or a corrupted image.")
72
73
74 def scale_image(image, size):
75 """Scales an image file if necessary."""
76
77 # don't upscale
78 if (size, size) >= image.size:
79 return image
80
81 (w, h) = image.size
82 if w > h:
83 diff = (w - h) / 2
84 image = image.crop((diff, 0, w - diff, h))
85 elif h > w:
86 diff = (h - w) / 2
87 image = image.crop((0, diff, w, h - diff))
88 image = image.resize((size, size), Image.ANTIALIAS)
89 return image
90
91
92 class UploadAvatarForm(forms.Form): 55 class UploadAvatarForm(forms.Form):
93 """Form used to change a user's avatar""" 56 """Form used to change a user's avatar"""
94 avatar_file = forms.ImageField(required=False) 57 avatar_file = forms.ImageField(required=False)
95 image = None 58 image = None
96 59
97 def clean_avatar_file(self): 60 def clean_avatar_file(self):
98 file = self.cleaned_data['avatar_file'] 61 f = self.cleaned_data['avatar_file']
99 if file is not None: 62 if f is not None:
100 if file.size > settings.MAX_AVATAR_SIZE_BYTES: 63 if f.size > settings.MAX_AVATAR_SIZE_BYTES:
101 raise forms.ValidationError("Please upload a file smaller than %s bytes." % \ 64 raise forms.ValidationError("Please upload a file smaller than "
102 settings.MAX_AVATAR_SIZE) 65 "%s bytes." % settings.MAX_AVATAR_SIZE)
103 self.image = get_image(file) 66 try:
104 self.format = self.image.format 67 self.image = parse_image(f)
105 return file 68 except IOError:
69 raise forms.ValidationError("Please upload a valid image. "
70 "The file you uploaded was either not an image or a "
71 "corrupted image.")
72 self.file_type = self.image.format
73 return f
106 74
107 def get_file(self): 75 def save(self):
108 if self.image is not None: 76 """
109 self.image = scale_image(self.image, settings.MAX_AVATAR_SIZE_PIXELS) 77 Perform any down-scaling needed on the new file, then return a tuple of
78 (filename, file object). Note that the file object returned may not
79 have a name; use the returned filename instead.
80
81 """
82 if not self.cleaned_data['avatar_file']:
83 return None, None
84
85 name = self.cleaned_data['avatar_file'].name
86 dim = settings.MAX_AVATAR_SIZE_PIXELS
87 max_size = (dim, dim)
88 if self.image and self.image.size > max_size:
89 self.image = downscale_image_square(self.image, dim)
90
91 # We need to return a Django File now. To get that from here,
92 # write the image data info a StringIO and then construct a
93 # Django ContentFile from that. The ContentFile has no name,
94 # that is why we return one ourselves explicitly.
110 s = StringIO() 95 s = StringIO()
111 self.image.save(s, self.format) 96 self.image.save(s, self.file_type)
112 return ContentFile(s.getvalue()) 97 return name, ContentFile(s.getvalue())
113 return None 98
114 99 return name, self.cleaned_data['avatar_file']
115 def get_filename(self):
116 return self.cleaned_data['avatar_file'].name
117 100
118 101
119 class SearchUsersForm(forms.Form): 102 class SearchUsersForm(forms.Form):
120 """ 103 """
121 A form to search for users. 104 A form to search for users.