Mercurial > public > madeira
comparison photologue/models.py @ 71:e2868ad47a1e
For Django 1.4, using the new manage.py.
author | Brian Neal <bgneal@gmail.com> |
---|---|
date | Sat, 14 Apr 2012 16:40:29 -0500 |
parents | madeira/photologue/models.py@63e4211628e1 |
children | dcd097a59223 |
comparison
equal
deleted
inserted
replaced
70:f26cdda0ad8b | 71:e2868ad47a1e |
---|---|
1 import os | |
2 import random | |
3 import shutil | |
4 import zipfile | |
5 | |
6 from datetime import datetime | |
7 from inspect import isclass | |
8 | |
9 from django.db import models | |
10 from django.db.models.signals import post_init | |
11 from django.conf import settings | |
12 from django.core.files.base import ContentFile | |
13 from django.core.urlresolvers import reverse | |
14 from django.template.defaultfilters import slugify | |
15 from django.utils.functional import curry | |
16 from django.utils.translation import ugettext_lazy as _ | |
17 | |
18 # Required PIL classes may or may not be available from the root namespace | |
19 # depending on the installation method used. | |
20 try: | |
21 import Image | |
22 import ImageFile | |
23 import ImageFilter | |
24 import ImageEnhance | |
25 except ImportError: | |
26 try: | |
27 from PIL import Image | |
28 from PIL import ImageFile | |
29 from PIL import ImageFilter | |
30 from PIL import ImageEnhance | |
31 except ImportError: | |
32 raise ImportError(_('Photologue was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.')) | |
33 | |
34 # attempt to load the django-tagging TagField from default location, | |
35 # otherwise we substitude a dummy TagField. | |
36 try: | |
37 from tagging.fields import TagField | |
38 tagfield_help_text = _('Separate tags with spaces, put quotes around multiple-word tags.') | |
39 except ImportError: | |
40 class TagField(models.CharField): | |
41 def __init__(self, **kwargs): | |
42 default_kwargs = {'max_length': 255, 'blank': True} | |
43 default_kwargs.update(kwargs) | |
44 super(TagField, self).__init__(**default_kwargs) | |
45 def get_internal_type(self): | |
46 return 'CharField' | |
47 tagfield_help_text = _('Django-tagging was not found, tags will be treated as plain text.') | |
48 | |
49 from utils import EXIF | |
50 from utils.reflection import add_reflection | |
51 from utils.watermark import apply_watermark | |
52 | |
53 # Path to sample image | |
54 SAMPLE_IMAGE_PATH = getattr(settings, 'SAMPLE_IMAGE_PATH', os.path.join(os.path.dirname(__file__), 'res', 'sample.jpg')) # os.path.join(settings.PROJECT_PATH, 'photologue', 'res', 'sample.jpg' | |
55 | |
56 # Modify image file buffer size. | |
57 ImageFile.MAXBLOCK = getattr(settings, 'PHOTOLOGUE_MAXBLOCK', 256 * 2 ** 10) | |
58 | |
59 # Photologue image path relative to media root | |
60 PHOTOLOGUE_DIR = getattr(settings, 'PHOTOLOGUE_DIR', 'photologue') | |
61 | |
62 # Look for user function to define file paths | |
63 PHOTOLOGUE_PATH = getattr(settings, 'PHOTOLOGUE_PATH', None) | |
64 if PHOTOLOGUE_PATH is not None: | |
65 if callable(PHOTOLOGUE_PATH): | |
66 get_storage_path = PHOTOLOGUE_PATH | |
67 else: | |
68 parts = PHOTOLOGUE_PATH.split('.') | |
69 module_name = '.'.join(parts[:-1]) | |
70 module = __import__(module_name) | |
71 get_storage_path = getattr(module, parts[-1]) | |
72 else: | |
73 def get_storage_path(instance, filename): | |
74 return os.path.join(PHOTOLOGUE_DIR, 'photos', filename) | |
75 | |
76 # Quality options for JPEG images | |
77 JPEG_QUALITY_CHOICES = ( | |
78 (30, _('Very Low')), | |
79 (40, _('Low')), | |
80 (50, _('Medium-Low')), | |
81 (60, _('Medium')), | |
82 (70, _('Medium-High')), | |
83 (80, _('High')), | |
84 (90, _('Very High')), | |
85 ) | |
86 | |
87 # choices for new crop_anchor field in Photo | |
88 CROP_ANCHOR_CHOICES = ( | |
89 ('top', _('Top')), | |
90 ('right', _('Right')), | |
91 ('bottom', _('Bottom')), | |
92 ('left', _('Left')), | |
93 ('center', _('Center (Default)')), | |
94 ) | |
95 | |
96 IMAGE_TRANSPOSE_CHOICES = ( | |
97 ('FLIP_LEFT_RIGHT', _('Flip left to right')), | |
98 ('FLIP_TOP_BOTTOM', _('Flip top to bottom')), | |
99 ('ROTATE_90', _('Rotate 90 degrees counter-clockwise')), | |
100 ('ROTATE_270', _('Rotate 90 degrees clockwise')), | |
101 ('ROTATE_180', _('Rotate 180 degrees')), | |
102 ) | |
103 | |
104 WATERMARK_STYLE_CHOICES = ( | |
105 ('tile', _('Tile')), | |
106 ('scale', _('Scale')), | |
107 ) | |
108 | |
109 # Prepare a list of image filters | |
110 filter_names = [] | |
111 for n in dir(ImageFilter): | |
112 klass = getattr(ImageFilter, n) | |
113 if isclass(klass) and issubclass(klass, ImageFilter.BuiltinFilter) and \ | |
114 hasattr(klass, 'name'): | |
115 filter_names.append(klass.__name__) | |
116 IMAGE_FILTERS_HELP_TEXT = _('Chain multiple filters using the following pattern "FILTER_ONE->FILTER_TWO->FILTER_THREE". Image filters will be applied in order. The following filters are available: %s.' % (', '.join(filter_names))) | |
117 | |
118 | |
119 class Gallery(models.Model): | |
120 date_added = models.DateTimeField(_('date published'), default=datetime.now) | |
121 title = models.CharField(_('title'), max_length=100, unique=True) | |
122 title_slug = models.SlugField(_('title slug'), unique=True, | |
123 help_text=_('A "slug" is a unique URL-friendly title for an object.')) | |
124 description = models.TextField(_('description'), blank=True) | |
125 is_public = models.BooleanField(_('is public'), default=True, | |
126 help_text=_('Public galleries will be displayed in the default views.')) | |
127 photos = models.ManyToManyField('Photo', related_name='galleries', verbose_name=_('photos'), | |
128 null=True, blank=True) | |
129 tags = TagField(help_text=tagfield_help_text, verbose_name=_('tags')) | |
130 | |
131 class Meta: | |
132 ordering = ['-date_added'] | |
133 get_latest_by = 'date_added' | |
134 verbose_name = _('gallery') | |
135 verbose_name_plural = _('galleries') | |
136 | |
137 def __unicode__(self): | |
138 return self.title | |
139 | |
140 def __str__(self): | |
141 return self.__unicode__() | |
142 | |
143 def get_absolute_url(self): | |
144 return reverse('pl-gallery', args=[self.title_slug]) | |
145 | |
146 def latest(self, limit=0, public=True): | |
147 if limit == 0: | |
148 limit = self.photo_count() | |
149 if public: | |
150 return self.public()[:limit] | |
151 else: | |
152 return self.photos.all()[:limit] | |
153 | |
154 def sample(self, count=0, public=True): | |
155 if count == 0 or count > self.photo_count(): | |
156 count = self.photo_count() | |
157 if public: | |
158 photo_set = self.public() | |
159 else: | |
160 photo_set = self.photos.all() | |
161 return random.sample(photo_set, count) | |
162 | |
163 def photo_count(self, public=True): | |
164 if public: | |
165 return self.public().count() | |
166 else: | |
167 return self.photos.all().count() | |
168 photo_count.short_description = _('count') | |
169 | |
170 def public(self): | |
171 return self.photos.filter(is_public=True) | |
172 | |
173 | |
174 class GalleryUpload(models.Model): | |
175 zip_file = models.FileField(_('images file (.zip)'), upload_to=PHOTOLOGUE_DIR+"/temp", | |
176 help_text=_('Select a .zip file of images to upload into a new Gallery.')) | |
177 gallery = models.ForeignKey(Gallery, null=True, blank=True, help_text=_('Select a gallery to add these images to. leave this empty to create a new gallery from the supplied title.')) | |
178 title = models.CharField(_('title'), max_length=75, help_text=_('All photos in the gallery will be given a title made up of the gallery title + a sequential number.')) | |
179 caption = models.TextField(_('caption'), blank=True, help_text=_('Caption will be added to all photos.')) | |
180 description = models.TextField(_('description'), blank=True, help_text=_('A description of this Gallery.')) | |
181 is_public = models.BooleanField(_('is public'), default=True, help_text=_('Uncheck this to make the uploaded gallery and included photographs private.')) | |
182 tags = models.CharField(max_length=255, blank=True, help_text=tagfield_help_text, verbose_name=_('tags')) | |
183 | |
184 class Meta: | |
185 verbose_name = _('gallery upload') | |
186 verbose_name_plural = _('gallery uploads') | |
187 | |
188 def save(self): | |
189 super(GalleryUpload, self).save() | |
190 self.process_zipfile() | |
191 super(GalleryUpload, self).delete() | |
192 | |
193 def process_zipfile(self): | |
194 if os.path.isfile(self.zip_file.path): | |
195 # TODO: implement try-except here | |
196 zip = zipfile.ZipFile(self.zip_file.path) | |
197 bad_file = zip.testzip() | |
198 if bad_file: | |
199 raise Exception('"%s" in the .zip archive is corrupt.' % bad_file) | |
200 count = 1 | |
201 if self.gallery: | |
202 gallery = self.gallery | |
203 else: | |
204 gallery = Gallery.objects.create(title=self.title, | |
205 title_slug=slugify(self.title), | |
206 description=self.description, | |
207 is_public=self.is_public, | |
208 tags=self.tags) | |
209 from cStringIO import StringIO | |
210 for filename in zip.namelist(): | |
211 if filename.startswith('__'): # do not process meta files | |
212 continue | |
213 data = zip.read(filename) | |
214 if len(data): | |
215 try: | |
216 # the following is taken from django.newforms.fields.ImageField: | |
217 # load() is the only method that can spot a truncated JPEG, | |
218 # but it cannot be called sanely after verify() | |
219 trial_image = Image.open(StringIO(data)) | |
220 trial_image.load() | |
221 # verify() is the only method that can spot a corrupt PNG, | |
222 # but it must be called immediately after the constructor | |
223 trial_image = Image.open(StringIO(data)) | |
224 trial_image.verify() | |
225 except Exception: | |
226 # if a "bad" file is found we just skip it. | |
227 continue | |
228 while 1: | |
229 title = ' '.join([self.title, str(count)]) | |
230 slug = slugify(title) | |
231 try: | |
232 p = Photo.objects.get(title_slug=slug) | |
233 except Photo.DoesNotExist: | |
234 photo = Photo(title=title, title_slug=slug, | |
235 caption=self.caption, | |
236 is_public=self.is_public, | |
237 tags=self.tags) | |
238 photo.image.save(filename, ContentFile(data)) | |
239 gallery.photos.add(photo) | |
240 count = count + 1 | |
241 break | |
242 count = count + 1 | |
243 zip.close() | |
244 | |
245 | |
246 class ImageModel(models.Model): | |
247 image = models.ImageField(_('image'), upload_to=get_storage_path) | |
248 date_taken = models.DateTimeField(_('date taken'), null=True, blank=True, editable=False) | |
249 view_count = models.PositiveIntegerField(default=0, editable=False) | |
250 crop_from = models.CharField(_('crop from'), blank=True, max_length=10, default='center', choices=CROP_ANCHOR_CHOICES) | |
251 effect = models.ForeignKey('PhotoEffect', null=True, blank=True, related_name="%(class)s_related", verbose_name=_('effect')) | |
252 | |
253 class Meta: | |
254 abstract = True | |
255 | |
256 @property | |
257 def EXIF(self): | |
258 try: | |
259 return EXIF.process_file(open(self.image.path, 'rb')) | |
260 except: | |
261 try: | |
262 return EXIF.process_file(open(self.image.path, 'rb'), details=False) | |
263 except: | |
264 return {} | |
265 | |
266 def admin_thumbnail(self): | |
267 func = getattr(self, 'get_admin_thumbnail_url', None) | |
268 if func is None: | |
269 return _('An "admin_thumbnail" photo size has not been defined.') | |
270 else: | |
271 if hasattr(self, 'get_absolute_url'): | |
272 return u'<a href="%s"><img src="%s"></a>' % \ | |
273 (self.get_absolute_url(), func()) | |
274 else: | |
275 return u'<a href="%s"><img src="%s"></a>' % \ | |
276 (self.image.url, func()) | |
277 admin_thumbnail.short_description = _('Thumbnail') | |
278 admin_thumbnail.allow_tags = True | |
279 | |
280 def cache_path(self): | |
281 return os.path.join(os.path.dirname(self.image.path), "cache") | |
282 | |
283 def cache_url(self): | |
284 return '/'.join([os.path.dirname(self.image.url), "cache"]) | |
285 | |
286 def image_filename(self): | |
287 return os.path.basename(self.image.path) | |
288 | |
289 def _get_filename_for_size(self, size): | |
290 size = getattr(size, 'name', size) | |
291 base, ext = os.path.splitext(self.image_filename()) | |
292 return ''.join([base, '_', size, ext]) | |
293 | |
294 def _get_SIZE_photosize(self, size): | |
295 return PhotoSizeCache().sizes.get(size) | |
296 | |
297 def _get_SIZE_size(self, size): | |
298 photosize = PhotoSizeCache().sizes.get(size) | |
299 if not self.size_exists(photosize): | |
300 self.create_size(photosize) | |
301 return Image.open(self._get_SIZE_filename(size)).size | |
302 | |
303 def _get_SIZE_url(self, size): | |
304 photosize = PhotoSizeCache().sizes.get(size) | |
305 if not self.size_exists(photosize): | |
306 self.create_size(photosize) | |
307 if photosize.increment_count: | |
308 self.view_count += 1 | |
309 self.save(update=True) | |
310 return '/'.join([self.cache_url(), self._get_filename_for_size(photosize.name)]) | |
311 | |
312 def _get_SIZE_filename(self, size): | |
313 photosize = PhotoSizeCache().sizes.get(size) | |
314 return os.path.join(self.cache_path(), | |
315 self._get_filename_for_size(photosize.name)) | |
316 | |
317 def add_accessor_methods(self, *args, **kwargs): | |
318 for size in PhotoSizeCache().sizes.keys(): | |
319 setattr(self, 'get_%s_size' % size, | |
320 curry(self._get_SIZE_size, size=size)) | |
321 setattr(self, 'get_%s_photosize' % size, | |
322 curry(self._get_SIZE_photosize, size=size)) | |
323 setattr(self, 'get_%s_url' % size, | |
324 curry(self._get_SIZE_url, size=size)) | |
325 setattr(self, 'get_%s_filename' % size, | |
326 curry(self._get_SIZE_filename, size=size)) | |
327 | |
328 def size_exists(self, photosize): | |
329 func = getattr(self, "get_%s_filename" % photosize.name, None) | |
330 if func is not None: | |
331 if os.path.isfile(func()): | |
332 return True | |
333 return False | |
334 | |
335 def resize_image(self, im, photosize): | |
336 cur_width, cur_height = im.size | |
337 new_width, new_height = photosize.size | |
338 if photosize.crop: | |
339 ratio = max(float(new_width)/cur_width,float(new_height)/cur_height) | |
340 x = (cur_width * ratio) | |
341 y = (cur_height * ratio) | |
342 xd = abs(new_width - x) | |
343 yd = abs(new_height - y) | |
344 x_diff = int(xd / 2) | |
345 y_diff = int(yd / 2) | |
346 if self.crop_from == 'top': | |
347 box = (int(x_diff), 0, int(x_diff+new_width), new_height) | |
348 elif self.crop_from == 'left': | |
349 box = (0, int(y_diff), new_width, int(y_diff+new_height)) | |
350 elif self.crop_from == 'bottom': | |
351 box = (int(x_diff), int(yd), int(x_diff+new_width), int(y)) # y - yd = new_height | |
352 elif self.crop_from == 'right': | |
353 box = (int(xd), int(y_diff), int(x), int(y_diff+new_height)) # x - xd = new_width | |
354 else: | |
355 box = (int(x_diff), int(y_diff), int(x_diff+new_width), int(y_diff+new_height)) | |
356 im = im.resize((int(x), int(y)), Image.ANTIALIAS).crop(box) | |
357 else: | |
358 if not new_width == 0 and not new_height == 0: | |
359 ratio = min(float(new_width)/cur_width, | |
360 float(new_height)/cur_height) | |
361 else: | |
362 if new_width == 0: | |
363 ratio = float(new_height)/cur_height | |
364 else: | |
365 ratio = float(new_width)/cur_width | |
366 new_dimensions = (int(round(cur_width*ratio)), | |
367 int(round(cur_height*ratio))) | |
368 if new_dimensions[0] > cur_width or \ | |
369 new_dimensions[1] > cur_height: | |
370 if not photosize.upscale: | |
371 return im | |
372 im = im.resize(new_dimensions, Image.ANTIALIAS) | |
373 return im | |
374 | |
375 def create_size(self, photosize): | |
376 if self.size_exists(photosize): | |
377 return | |
378 if not os.path.isdir(self.cache_path()): | |
379 os.makedirs(self.cache_path()) | |
380 try: | |
381 im = Image.open(self.image.path) | |
382 except IOError: | |
383 return | |
384 # Apply effect if found | |
385 if self.effect is not None: | |
386 im = self.effect.pre_process(im) | |
387 elif photosize.effect is not None: | |
388 im = photosize.effect.pre_process(im) | |
389 # Resize/crop image | |
390 if im.size != photosize.size: | |
391 im = self.resize_image(im, photosize) | |
392 # Apply watermark if found | |
393 if photosize.watermark is not None: | |
394 im = photosize.watermark.post_process(im) | |
395 # Apply effect if found | |
396 if self.effect is not None: | |
397 im = self.effect.post_process(im) | |
398 elif photosize.effect is not None: | |
399 im = photosize.effect.post_process(im) | |
400 # Save file | |
401 im_filename = getattr(self, "get_%s_filename" % photosize.name)() | |
402 try: | |
403 if im.format == 'JPEG': | |
404 im.save(im_filename, 'JPEG', quality=int(photosize.quality), optimize=True) | |
405 else: | |
406 im.save(im_filename) | |
407 except IOError, e: | |
408 if os.path.isfile(im_filename): | |
409 os.unlink(im_filename) | |
410 raise e | |
411 | |
412 def remove_size(self, photosize, remove_dirs=True): | |
413 if not self.size_exists(photosize): | |
414 return | |
415 filename = getattr(self, "get_%s_filename" % photosize.name)() | |
416 if os.path.isfile(filename): | |
417 os.remove(filename) | |
418 if remove_dirs: | |
419 self.remove_cache_dirs() | |
420 | |
421 def clear_cache(self): | |
422 cache = PhotoSizeCache() | |
423 for photosize in cache.sizes.values(): | |
424 self.remove_size(photosize, False) | |
425 self.remove_cache_dirs() | |
426 | |
427 def pre_cache(self): | |
428 cache = PhotoSizeCache() | |
429 for photosize in cache.sizes.values(): | |
430 if photosize.pre_cache: | |
431 self.create_size(photosize) | |
432 | |
433 def remove_cache_dirs(self): | |
434 try: | |
435 os.removedirs(self.cache_path()) | |
436 except: | |
437 pass | |
438 | |
439 def save(self, update=False): | |
440 if update: | |
441 models.Model.save(self) | |
442 return | |
443 if self.date_taken is None: | |
444 try: | |
445 exif_date = self.EXIF.get('EXIF DateTimeOriginal', None) | |
446 if exif_date is not None: | |
447 d, t = str.split(exif_date.values) | |
448 year, month, day = d.split(':') | |
449 hour, minute, second = t.split(':') | |
450 self.date_taken = datetime(int(year), int(month), int(day), | |
451 int(hour), int(minute), int(second)) | |
452 except: | |
453 pass | |
454 if self.date_taken is None: | |
455 self.date_taken = datetime.now() | |
456 if self._get_pk_val(): | |
457 self.clear_cache() | |
458 super(ImageModel, self).save() | |
459 self.pre_cache() | |
460 | |
461 def delete(self): | |
462 self.clear_cache() | |
463 super(ImageModel, self).delete() | |
464 | |
465 | |
466 class Photo(ImageModel): | |
467 title = models.CharField(_('title'), max_length=100, unique=True) | |
468 title_slug = models.SlugField(_('slug'), unique=True, | |
469 help_text=('A "slug" is a unique URL-friendly title for an object.')) | |
470 caption = models.TextField(_('caption'), blank=True) | |
471 date_added = models.DateTimeField(_('date added'), default=datetime.now, editable=False) | |
472 is_public = models.BooleanField(_('is public'), default=True, help_text=_('Public photographs will be displayed in the default views.')) | |
473 tags = TagField(help_text=tagfield_help_text, verbose_name=_('tags')) | |
474 | |
475 class Meta: | |
476 ordering = ['-date_added'] | |
477 get_latest_by = 'date_added' | |
478 verbose_name = _("photo") | |
479 verbose_name_plural = _("photos") | |
480 | |
481 def __unicode__(self): | |
482 return self.title | |
483 | |
484 def __str__(self): | |
485 return self.__unicode__() | |
486 | |
487 def save(self, update=False): | |
488 if self.title_slug is None: | |
489 self.title_slug = slugify(self.title) | |
490 super(Photo, self).save(update) | |
491 | |
492 def get_absolute_url(self): | |
493 return reverse('pl-photo', args=[self.title_slug]) | |
494 | |
495 def public_galleries(self): | |
496 """Return the public galleries to which this photo belongs.""" | |
497 return self.galleries.filter(is_public=True) | |
498 | |
499 | |
500 class BaseEffect(models.Model): | |
501 name = models.CharField(_('name'), max_length=30, unique=True) | |
502 description = models.TextField(_('description'), blank=True) | |
503 | |
504 class Meta: | |
505 abstract = True | |
506 | |
507 def sample_dir(self): | |
508 return os.path.join(settings.MEDIA_ROOT, PHOTOLOGUE_DIR, 'samples') | |
509 | |
510 def sample_url(self): | |
511 return settings.MEDIA_URL + '/'.join([PHOTOLOGUE_DIR, 'samples', '%s %s.jpg' % (self.name.lower(), 'sample')]) | |
512 | |
513 def sample_filename(self): | |
514 return os.path.join(self.sample_dir(), '%s %s.jpg' % (self.name.lower(), 'sample')) | |
515 | |
516 def create_sample(self): | |
517 if not os.path.isdir(self.sample_dir()): | |
518 os.makedirs(self.sample_dir()) | |
519 try: | |
520 im = Image.open(SAMPLE_IMAGE_PATH) | |
521 except IOError: | |
522 raise IOError('Photologue was unable to open the sample image: %s.' % SAMPLE_IMAGE_PATH) | |
523 im = self.process(im) | |
524 im.save(self.sample_filename(), 'JPEG', quality=90, optimize=True) | |
525 | |
526 def admin_sample(self): | |
527 return u'<img src="%s">' % self.sample_url() | |
528 admin_sample.short_description = 'Sample' | |
529 admin_sample.allow_tags = True | |
530 | |
531 def pre_process(self, im): | |
532 return im | |
533 | |
534 def post_process(self, im): | |
535 return im | |
536 | |
537 def process(self, im): | |
538 im = self.pre_process(im) | |
539 im = self.post_process(im) | |
540 return im | |
541 | |
542 def __unicode__(self): | |
543 return self.name | |
544 | |
545 def __str__(self): | |
546 return self.__unicode__() | |
547 | |
548 def save(self): | |
549 try: | |
550 os.remove(self.sample_filename()) | |
551 except: | |
552 pass | |
553 models.Model.save(self) | |
554 self.create_sample() | |
555 for size in self.photo_sizes.all(): | |
556 size.clear_cache() | |
557 # try to clear all related subclasses of ImageModel | |
558 for prop in [prop for prop in dir(self) if prop[-8:] == '_related']: | |
559 for obj in getattr(self, prop).all(): | |
560 obj.clear_cache() | |
561 obj.pre_cache() | |
562 | |
563 def delete(self): | |
564 try: | |
565 os.remove(self.sample_filename()) | |
566 except: | |
567 pass | |
568 super(PhotoEffect, self).delete() | |
569 | |
570 | |
571 class PhotoEffect(BaseEffect): | |
572 """ A pre-defined effect to apply to photos """ | |
573 transpose_method = models.CharField(_('rotate or flip'), max_length=15, blank=True, choices=IMAGE_TRANSPOSE_CHOICES) | |
574 color = models.FloatField(_('color'), default=1.0, help_text=_("A factor of 0.0 gives a black and white image, a factor of 1.0 gives the original image.")) | |
575 brightness = models.FloatField(_('brightness'), default=1.0, help_text=_("A factor of 0.0 gives a black image, a factor of 1.0 gives the original image.")) | |
576 contrast = models.FloatField(_('contrast'), default=1.0, help_text=_("A factor of 0.0 gives a solid grey image, a factor of 1.0 gives the original image.")) | |
577 sharpness = models.FloatField(_('sharpness'), default=1.0, help_text=_("A factor of 0.0 gives a blurred image, a factor of 1.0 gives the original image.")) | |
578 filters = models.CharField(_('filters'), max_length=200, blank=True, help_text=_(IMAGE_FILTERS_HELP_TEXT)) | |
579 reflection_size = models.FloatField(_('size'), default=0, help_text=_("The height of the reflection as a percentage of the orignal image. A factor of 0.0 adds no reflection, a factor of 1.0 adds a reflection equal to the height of the orignal image.")) | |
580 reflection_strength = models.FloatField(_('strength'), default=0.6, help_text="The initial opacity of the reflection gradient.") | |
581 background_color = models.CharField(_('color'), max_length=7, default="#FFFFFF", help_text="The background color of the reflection gradient. Set this to match the background color of your page.") | |
582 | |
583 class Meta: | |
584 verbose_name = _("photo effect") | |
585 verbose_name_plural = _("photo effects") | |
586 | |
587 def pre_process(self, im): | |
588 if self.transpose_method != '': | |
589 method = getattr(Image, self.transpose_method) | |
590 im = im.transpose(method) | |
591 if im.mode != 'RGB' and im.mode != 'RGBA': | |
592 return im | |
593 for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']: | |
594 factor = getattr(self, name.lower()) | |
595 if factor != 1.0: | |
596 im = getattr(ImageEnhance, name)(im).enhance(factor) | |
597 for name in self.filters.split('->'): | |
598 image_filter = getattr(ImageFilter, name.upper(), None) | |
599 if image_filter is not None: | |
600 try: | |
601 im = im.filter(image_filter) | |
602 except ValueError: | |
603 pass | |
604 return im | |
605 | |
606 def post_process(self, im): | |
607 if self.reflection_size != 0.0: | |
608 im = add_reflection(im, bgcolor=self.background_color, amount=self.reflection_size, opacity=self.reflection_strength) | |
609 return im | |
610 | |
611 | |
612 class Watermark(BaseEffect): | |
613 image = models.ImageField(_('image'), upload_to=PHOTOLOGUE_DIR+"/watermarks") | |
614 style = models.CharField(_('style'), max_length=5, choices=WATERMARK_STYLE_CHOICES, default='scale') | |
615 opacity = models.FloatField(_('opacity'), default=1, help_text=_("The opacity of the overlay.")) | |
616 | |
617 class Meta: | |
618 verbose_name = _('watermark') | |
619 verbose_name_plural = _('watermarks') | |
620 | |
621 def post_process(self, im): | |
622 mark = Image.open(self.image.path) | |
623 return apply_watermark(im, mark, self.style, self.opacity) | |
624 | |
625 | |
626 class PhotoSize(models.Model): | |
627 name = models.CharField(_('name'), max_length=20, unique=True, help_text=_('Photo size name should contain only letters, numbers and underscores. Examples: "thumbnail", "display", "small", "main_page_widget".')) | |
628 width = models.PositiveIntegerField(_('width'), default=0, help_text=_('If width is set to "0" the image will be scaled to the supplied height.')) | |
629 height = models.PositiveIntegerField(_('height'), default=0, help_text=_('If height is set to "0" the image will be scaled to the supplied width')) | |
630 quality = models.PositiveIntegerField(_('quality'), choices=JPEG_QUALITY_CHOICES, default=70, help_text=_('JPEG image quality.')) | |
631 upscale = models.BooleanField(_('upscale images?'), default=False, help_text=_('If selected the image will be scaled up if necessary to fit the supplied dimensions. Cropped sizes will be upscaled regardless of this setting.')) | |
632 crop = models.BooleanField(_('crop to fit?'), default=False, help_text=_('If selected the image will be scaled and cropped to fit the supplied dimensions.')) | |
633 pre_cache = models.BooleanField(_('pre-cache?'), default=False, help_text=_('If selected this photo size will be pre-cached as photos are added.')) | |
634 increment_count = models.BooleanField(_('increment view count?'), default=False, help_text=_('If selected the image\'s "view_count" will be incremented when this photo size is displayed.')) | |
635 effect = models.ForeignKey('PhotoEffect', null=True, blank=True, related_name='photo_sizes', verbose_name=_('photo effect')) | |
636 watermark = models.ForeignKey('Watermark', null=True, blank=True, related_name='photo_sizes', verbose_name=_('watermark image')) | |
637 | |
638 class Meta: | |
639 ordering = ['width', 'height'] | |
640 verbose_name = _('photo size') | |
641 verbose_name_plural = _('photo sizes') | |
642 | |
643 def __unicode__(self): | |
644 return self.name | |
645 | |
646 def __str__(self): | |
647 return self.__unicode__() | |
648 | |
649 def clear_cache(self): | |
650 for cls in ImageModel.__subclasses__(): | |
651 for obj in cls.objects.all(): | |
652 obj.remove_size(self) | |
653 if self.pre_cache: | |
654 obj.create_size(self) | |
655 PhotoSizeCache().reset() | |
656 | |
657 def save(self): | |
658 if self.width + self.height <= 0: | |
659 raise ValueError(_('A PhotoSize must have a positive height or width.')) | |
660 super(PhotoSize, self).save() | |
661 PhotoSizeCache().reset() | |
662 self.clear_cache() | |
663 | |
664 def delete(self): | |
665 self.clear_cache() | |
666 super(PhotoSize, self).delete() | |
667 | |
668 def _get_size(self): | |
669 return (self.width, self.height) | |
670 def _set_size(self, value): | |
671 self.width, self.height = value | |
672 size = property(_get_size, _set_size) | |
673 | |
674 | |
675 class PhotoSizeCache(object): | |
676 __state = {"sizes": {}} | |
677 | |
678 def __init__(self): | |
679 self.__dict__ = self.__state | |
680 if not len(self.sizes): | |
681 sizes = PhotoSize.objects.all() | |
682 for size in sizes: | |
683 self.sizes[size.name] = size | |
684 | |
685 def reset(self): | |
686 self.sizes = {} | |
687 | |
688 | |
689 # Set up the accessor methods | |
690 def add_methods(sender, instance, signal, *args, **kwargs): | |
691 """ Adds methods to access sized images (urls, paths) | |
692 | |
693 after the Photo model's __init__ function completes, | |
694 this method calls "add_accessor_methods" on each instance. | |
695 """ | |
696 if hasattr(instance, 'add_accessor_methods'): | |
697 instance.add_accessor_methods() | |
698 | |
699 # connect the add_accessor_methods function to the post_init signal | |
700 post_init.connect(add_methods) |