bgneal@1: import os bgneal@1: import random bgneal@1: import zipfile bgneal@1: bgneal@1: from datetime import datetime bgneal@1: from inspect import isclass bgneal@1: bgneal@1: from django.db import models bgneal@1: from django.db.models.signals import post_init bgneal@1: from django.conf import settings bgneal@1: from django.core.files.base import ContentFile bgneal@1: from django.core.urlresolvers import reverse bgneal@94: from django.utils.text import slugify bgneal@1: from django.utils.functional import curry bgneal@1: from django.utils.translation import ugettext_lazy as _ bgneal@151: from django.utils.http import urlquote bgneal@1: bgneal@1: # Required PIL classes may or may not be available from the root namespace bgneal@1: # depending on the installation method used. bgneal@1: try: bgneal@1: import Image bgneal@1: import ImageFile bgneal@1: import ImageFilter bgneal@1: import ImageEnhance bgneal@1: except ImportError: bgneal@1: try: bgneal@1: from PIL import Image bgneal@1: from PIL import ImageFile bgneal@1: from PIL import ImageFilter bgneal@1: from PIL import ImageEnhance bgneal@1: except ImportError: bgneal@1: raise ImportError(_('Photologue was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.')) bgneal@1: bgneal@1: # attempt to load the django-tagging TagField from default location, bgneal@1: # otherwise we substitude a dummy TagField. bgneal@1: try: bgneal@1: from tagging.fields import TagField bgneal@1: tagfield_help_text = _('Separate tags with spaces, put quotes around multiple-word tags.') bgneal@1: except ImportError: bgneal@1: class TagField(models.CharField): bgneal@1: def __init__(self, **kwargs): bgneal@1: default_kwargs = {'max_length': 255, 'blank': True} bgneal@1: default_kwargs.update(kwargs) bgneal@1: super(TagField, self).__init__(**default_kwargs) bgneal@1: def get_internal_type(self): bgneal@1: return 'CharField' bgneal@1: tagfield_help_text = _('Django-tagging was not found, tags will be treated as plain text.') bgneal@1: bgneal@1: from utils import EXIF bgneal@1: from utils.reflection import add_reflection bgneal@1: from utils.watermark import apply_watermark bgneal@1: bgneal@1: # Path to sample image bgneal@1: 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' bgneal@1: bgneal@1: # Modify image file buffer size. bgneal@1: ImageFile.MAXBLOCK = getattr(settings, 'PHOTOLOGUE_MAXBLOCK', 256 * 2 ** 10) bgneal@1: bgneal@1: # Photologue image path relative to media root bgneal@1: PHOTOLOGUE_DIR = getattr(settings, 'PHOTOLOGUE_DIR', 'photologue') bgneal@1: bgneal@1: # Look for user function to define file paths bgneal@1: PHOTOLOGUE_PATH = getattr(settings, 'PHOTOLOGUE_PATH', None) bgneal@1: if PHOTOLOGUE_PATH is not None: bgneal@1: if callable(PHOTOLOGUE_PATH): bgneal@1: get_storage_path = PHOTOLOGUE_PATH bgneal@1: else: bgneal@1: parts = PHOTOLOGUE_PATH.split('.') bgneal@1: module_name = '.'.join(parts[:-1]) bgneal@1: module = __import__(module_name) bgneal@1: get_storage_path = getattr(module, parts[-1]) bgneal@1: else: bgneal@1: def get_storage_path(instance, filename): bgneal@1: return os.path.join(PHOTOLOGUE_DIR, 'photos', filename) bgneal@1: bgneal@1: # Quality options for JPEG images bgneal@1: JPEG_QUALITY_CHOICES = ( bgneal@1: (30, _('Very Low')), bgneal@1: (40, _('Low')), bgneal@1: (50, _('Medium-Low')), bgneal@1: (60, _('Medium')), bgneal@1: (70, _('Medium-High')), bgneal@1: (80, _('High')), bgneal@1: (90, _('Very High')), bgneal@1: ) bgneal@1: bgneal@1: # choices for new crop_anchor field in Photo bgneal@1: CROP_ANCHOR_CHOICES = ( bgneal@1: ('top', _('Top')), bgneal@1: ('right', _('Right')), bgneal@1: ('bottom', _('Bottom')), bgneal@1: ('left', _('Left')), bgneal@1: ('center', _('Center (Default)')), bgneal@1: ) bgneal@1: bgneal@1: IMAGE_TRANSPOSE_CHOICES = ( bgneal@1: ('FLIP_LEFT_RIGHT', _('Flip left to right')), bgneal@1: ('FLIP_TOP_BOTTOM', _('Flip top to bottom')), bgneal@1: ('ROTATE_90', _('Rotate 90 degrees counter-clockwise')), bgneal@1: ('ROTATE_270', _('Rotate 90 degrees clockwise')), bgneal@1: ('ROTATE_180', _('Rotate 180 degrees')), bgneal@1: ) bgneal@1: bgneal@1: WATERMARK_STYLE_CHOICES = ( bgneal@1: ('tile', _('Tile')), bgneal@1: ('scale', _('Scale')), bgneal@1: ) bgneal@1: bgneal@1: # Prepare a list of image filters bgneal@1: filter_names = [] bgneal@1: for n in dir(ImageFilter): bgneal@1: klass = getattr(ImageFilter, n) bgneal@1: if isclass(klass) and issubclass(klass, ImageFilter.BuiltinFilter) and \ bgneal@1: hasattr(klass, 'name'): bgneal@1: filter_names.append(klass.__name__) bgneal@1: 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))) bgneal@1: bgneal@1: bgneal@1: class Gallery(models.Model): bgneal@1: date_added = models.DateTimeField(_('date published'), default=datetime.now) bgneal@1: title = models.CharField(_('title'), max_length=100, unique=True) bgneal@1: title_slug = models.SlugField(_('title slug'), unique=True, bgneal@1: help_text=_('A "slug" is a unique URL-friendly title for an object.')) bgneal@1: description = models.TextField(_('description'), blank=True) bgneal@1: is_public = models.BooleanField(_('is public'), default=True, bgneal@1: help_text=_('Public galleries will be displayed in the default views.')) bgneal@1: photos = models.ManyToManyField('Photo', related_name='galleries', verbose_name=_('photos'), bgneal@186: blank=True) bgneal@1: tags = TagField(help_text=tagfield_help_text, verbose_name=_('tags')) bgneal@1: bgneal@1: class Meta: bgneal@1: ordering = ['-date_added'] bgneal@1: get_latest_by = 'date_added' bgneal@1: verbose_name = _('gallery') bgneal@1: verbose_name_plural = _('galleries') bgneal@1: bgneal@1: def __unicode__(self): bgneal@1: return self.title bgneal@1: bgneal@1: def __str__(self): bgneal@1: return self.__unicode__() bgneal@1: bgneal@1: def get_absolute_url(self): bgneal@1: return reverse('pl-gallery', args=[self.title_slug]) bgneal@1: bgneal@1: def latest(self, limit=0, public=True): bgneal@1: if limit == 0: bgneal@1: limit = self.photo_count() bgneal@1: if public: bgneal@1: return self.public()[:limit] bgneal@1: else: bgneal@1: return self.photos.all()[:limit] bgneal@1: bgneal@1: def sample(self, count=0, public=True): bgneal@1: if count == 0 or count > self.photo_count(): bgneal@1: count = self.photo_count() bgneal@1: if public: bgneal@1: photo_set = self.public() bgneal@1: else: bgneal@1: photo_set = self.photos.all() bgneal@1: return random.sample(photo_set, count) bgneal@1: bgneal@1: def photo_count(self, public=True): bgneal@1: if public: bgneal@1: return self.public().count() bgneal@1: else: bgneal@1: return self.photos.all().count() bgneal@1: photo_count.short_description = _('count') bgneal@1: bgneal@1: def public(self): bgneal@1: return self.photos.filter(is_public=True) bgneal@1: bgneal@1: bgneal@1: class GalleryUpload(models.Model): bgneal@1: zip_file = models.FileField(_('images file (.zip)'), upload_to=PHOTOLOGUE_DIR+"/temp", bgneal@1: help_text=_('Select a .zip file of images to upload into a new Gallery.')) bgneal@1: 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.')) bgneal@1: 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.')) bgneal@1: caption = models.TextField(_('caption'), blank=True, help_text=_('Caption will be added to all photos.')) bgneal@1: description = models.TextField(_('description'), blank=True, help_text=_('A description of this Gallery.')) bgneal@1: is_public = models.BooleanField(_('is public'), default=True, help_text=_('Uncheck this to make the uploaded gallery and included photographs private.')) bgneal@1: tags = models.CharField(max_length=255, blank=True, help_text=tagfield_help_text, verbose_name=_('tags')) bgneal@1: bgneal@1: class Meta: bgneal@1: verbose_name = _('gallery upload') bgneal@1: verbose_name_plural = _('gallery uploads') bgneal@1: bgneal@1: def save(self): bgneal@1: super(GalleryUpload, self).save() bgneal@1: self.process_zipfile() bgneal@1: super(GalleryUpload, self).delete() bgneal@1: bgneal@1: def process_zipfile(self): bgneal@1: if os.path.isfile(self.zip_file.path): bgneal@1: # TODO: implement try-except here bgneal@1: zip = zipfile.ZipFile(self.zip_file.path) bgneal@1: bad_file = zip.testzip() bgneal@1: if bad_file: bgneal@1: raise Exception('"%s" in the .zip archive is corrupt.' % bad_file) bgneal@1: count = 1 bgneal@1: if self.gallery: bgneal@1: gallery = self.gallery bgneal@1: else: bgneal@1: gallery = Gallery.objects.create(title=self.title, bgneal@1: title_slug=slugify(self.title), bgneal@1: description=self.description, bgneal@1: is_public=self.is_public, bgneal@1: tags=self.tags) bgneal@1: from cStringIO import StringIO bgneal@1: for filename in zip.namelist(): bgneal@1: if filename.startswith('__'): # do not process meta files bgneal@1: continue bgneal@1: data = zip.read(filename) bgneal@1: if len(data): bgneal@1: try: bgneal@1: # the following is taken from django.newforms.fields.ImageField: bgneal@1: # load() is the only method that can spot a truncated JPEG, bgneal@1: # but it cannot be called sanely after verify() bgneal@1: trial_image = Image.open(StringIO(data)) bgneal@1: trial_image.load() bgneal@1: # verify() is the only method that can spot a corrupt PNG, bgneal@1: # but it must be called immediately after the constructor bgneal@1: trial_image = Image.open(StringIO(data)) bgneal@1: trial_image.verify() bgneal@1: except Exception: bgneal@1: # if a "bad" file is found we just skip it. bgneal@1: continue bgneal@1: while 1: bgneal@1: title = ' '.join([self.title, str(count)]) bgneal@1: slug = slugify(title) bgneal@1: try: bgneal@1: p = Photo.objects.get(title_slug=slug) bgneal@94: except Photo.DoesNotExist: bgneal@1: photo = Photo(title=title, title_slug=slug, bgneal@1: caption=self.caption, bgneal@1: is_public=self.is_public, bgneal@1: tags=self.tags) bgneal@1: photo.image.save(filename, ContentFile(data)) bgneal@1: gallery.photos.add(photo) bgneal@1: count = count + 1 bgneal@1: break bgneal@1: count = count + 1 bgneal@1: zip.close() bgneal@1: bgneal@1: bgneal@1: class ImageModel(models.Model): bgneal@1: image = models.ImageField(_('image'), upload_to=get_storage_path) bgneal@1: date_taken = models.DateTimeField(_('date taken'), null=True, blank=True, editable=False) bgneal@1: view_count = models.PositiveIntegerField(default=0, editable=False) bgneal@1: crop_from = models.CharField(_('crop from'), blank=True, max_length=10, default='center', choices=CROP_ANCHOR_CHOICES) bgneal@1: effect = models.ForeignKey('PhotoEffect', null=True, blank=True, related_name="%(class)s_related", verbose_name=_('effect')) bgneal@1: bgneal@1: class Meta: bgneal@1: abstract = True bgneal@1: bgneal@1: @property bgneal@1: def EXIF(self): bgneal@1: try: bgneal@1: return EXIF.process_file(open(self.image.path, 'rb')) bgneal@1: except: bgneal@1: try: bgneal@1: return EXIF.process_file(open(self.image.path, 'rb'), details=False) bgneal@1: except: bgneal@1: return {} bgneal@1: bgneal@1: def admin_thumbnail(self): bgneal@1: func = getattr(self, 'get_admin_thumbnail_url', None) bgneal@1: if func is None: bgneal@1: return _('An "admin_thumbnail" photo size has not been defined.') bgneal@1: else: bgneal@1: if hasattr(self, 'get_absolute_url'): bgneal@1: return u'' % \ bgneal@1: (self.get_absolute_url(), func()) bgneal@1: else: bgneal@1: return u'' % \ bgneal@1: (self.image.url, func()) bgneal@1: admin_thumbnail.short_description = _('Thumbnail') bgneal@1: admin_thumbnail.allow_tags = True bgneal@1: bgneal@1: def cache_path(self): bgneal@1: return os.path.join(os.path.dirname(self.image.path), "cache") bgneal@1: bgneal@1: def cache_url(self): bgneal@1: return '/'.join([os.path.dirname(self.image.url), "cache"]) bgneal@1: bgneal@1: def image_filename(self): bgneal@1: return os.path.basename(self.image.path) bgneal@1: bgneal@1: def _get_filename_for_size(self, size): bgneal@1: size = getattr(size, 'name', size) bgneal@1: base, ext = os.path.splitext(self.image_filename()) bgneal@1: return ''.join([base, '_', size, ext]) bgneal@1: bgneal@1: def _get_SIZE_photosize(self, size): bgneal@1: return PhotoSizeCache().sizes.get(size) bgneal@1: bgneal@1: def _get_SIZE_size(self, size): bgneal@1: photosize = PhotoSizeCache().sizes.get(size) bgneal@1: if not self.size_exists(photosize): bgneal@1: self.create_size(photosize) bgneal@1: return Image.open(self._get_SIZE_filename(size)).size bgneal@1: bgneal@1: def _get_SIZE_url(self, size): bgneal@1: photosize = PhotoSizeCache().sizes.get(size) bgneal@1: if not self.size_exists(photosize): bgneal@1: self.create_size(photosize) bgneal@1: if photosize.increment_count: bgneal@1: self.view_count += 1 bgneal@1: self.save(update=True) bgneal@151: return '/'.join([self.cache_url(), bgneal@151: urlquote(self._get_filename_for_size(photosize.name))]) bgneal@1: bgneal@1: def _get_SIZE_filename(self, size): bgneal@1: photosize = PhotoSizeCache().sizes.get(size) bgneal@1: return os.path.join(self.cache_path(), bgneal@1: self._get_filename_for_size(photosize.name)) bgneal@1: bgneal@1: def add_accessor_methods(self, *args, **kwargs): bgneal@1: for size in PhotoSizeCache().sizes.keys(): bgneal@1: setattr(self, 'get_%s_size' % size, bgneal@1: curry(self._get_SIZE_size, size=size)) bgneal@1: setattr(self, 'get_%s_photosize' % size, bgneal@1: curry(self._get_SIZE_photosize, size=size)) bgneal@1: setattr(self, 'get_%s_url' % size, bgneal@1: curry(self._get_SIZE_url, size=size)) bgneal@1: setattr(self, 'get_%s_filename' % size, bgneal@1: curry(self._get_SIZE_filename, size=size)) bgneal@1: bgneal@1: def size_exists(self, photosize): bgneal@1: func = getattr(self, "get_%s_filename" % photosize.name, None) bgneal@1: if func is not None: bgneal@1: if os.path.isfile(func()): bgneal@1: return True bgneal@1: return False bgneal@1: bgneal@1: def resize_image(self, im, photosize): bgneal@1: cur_width, cur_height = im.size bgneal@1: new_width, new_height = photosize.size bgneal@1: if photosize.crop: bgneal@1: ratio = max(float(new_width)/cur_width,float(new_height)/cur_height) bgneal@1: x = (cur_width * ratio) bgneal@1: y = (cur_height * ratio) bgneal@1: xd = abs(new_width - x) bgneal@1: yd = abs(new_height - y) bgneal@1: x_diff = int(xd / 2) bgneal@1: y_diff = int(yd / 2) bgneal@1: if self.crop_from == 'top': bgneal@1: box = (int(x_diff), 0, int(x_diff+new_width), new_height) bgneal@1: elif self.crop_from == 'left': bgneal@1: box = (0, int(y_diff), new_width, int(y_diff+new_height)) bgneal@1: elif self.crop_from == 'bottom': bgneal@1: box = (int(x_diff), int(yd), int(x_diff+new_width), int(y)) # y - yd = new_height bgneal@1: elif self.crop_from == 'right': bgneal@1: box = (int(xd), int(y_diff), int(x), int(y_diff+new_height)) # x - xd = new_width bgneal@1: else: bgneal@1: box = (int(x_diff), int(y_diff), int(x_diff+new_width), int(y_diff+new_height)) bgneal@1: im = im.resize((int(x), int(y)), Image.ANTIALIAS).crop(box) bgneal@1: else: bgneal@1: if not new_width == 0 and not new_height == 0: bgneal@1: ratio = min(float(new_width)/cur_width, bgneal@1: float(new_height)/cur_height) bgneal@1: else: bgneal@1: if new_width == 0: bgneal@1: ratio = float(new_height)/cur_height bgneal@1: else: bgneal@1: ratio = float(new_width)/cur_width bgneal@1: new_dimensions = (int(round(cur_width*ratio)), bgneal@1: int(round(cur_height*ratio))) bgneal@1: if new_dimensions[0] > cur_width or \ bgneal@1: new_dimensions[1] > cur_height: bgneal@1: if not photosize.upscale: bgneal@1: return im bgneal@1: im = im.resize(new_dimensions, Image.ANTIALIAS) bgneal@1: return im bgneal@1: bgneal@1: def create_size(self, photosize): bgneal@1: if self.size_exists(photosize): bgneal@1: return bgneal@1: if not os.path.isdir(self.cache_path()): bgneal@1: os.makedirs(self.cache_path()) bgneal@1: try: bgneal@1: im = Image.open(self.image.path) bgneal@1: except IOError: bgneal@1: return bgneal@1: # Apply effect if found bgneal@1: if self.effect is not None: bgneal@1: im = self.effect.pre_process(im) bgneal@1: elif photosize.effect is not None: bgneal@1: im = photosize.effect.pre_process(im) bgneal@1: # Resize/crop image bgneal@1: if im.size != photosize.size: bgneal@1: im = self.resize_image(im, photosize) bgneal@1: # Apply watermark if found bgneal@1: if photosize.watermark is not None: bgneal@1: im = photosize.watermark.post_process(im) bgneal@1: # Apply effect if found bgneal@1: if self.effect is not None: bgneal@1: im = self.effect.post_process(im) bgneal@1: elif photosize.effect is not None: bgneal@1: im = photosize.effect.post_process(im) bgneal@1: # Save file bgneal@1: im_filename = getattr(self, "get_%s_filename" % photosize.name)() bgneal@1: try: bgneal@1: if im.format == 'JPEG': bgneal@1: im.save(im_filename, 'JPEG', quality=int(photosize.quality), optimize=True) bgneal@1: else: bgneal@1: im.save(im_filename) bgneal@1: except IOError, e: bgneal@1: if os.path.isfile(im_filename): bgneal@1: os.unlink(im_filename) bgneal@1: raise e bgneal@1: bgneal@1: def remove_size(self, photosize, remove_dirs=True): bgneal@1: if not self.size_exists(photosize): bgneal@1: return bgneal@1: filename = getattr(self, "get_%s_filename" % photosize.name)() bgneal@1: if os.path.isfile(filename): bgneal@1: os.remove(filename) bgneal@1: if remove_dirs: bgneal@1: self.remove_cache_dirs() bgneal@1: bgneal@1: def clear_cache(self): bgneal@1: cache = PhotoSizeCache() bgneal@1: for photosize in cache.sizes.values(): bgneal@1: self.remove_size(photosize, False) bgneal@1: self.remove_cache_dirs() bgneal@1: bgneal@1: def pre_cache(self): bgneal@1: cache = PhotoSizeCache() bgneal@1: for photosize in cache.sizes.values(): bgneal@1: if photosize.pre_cache: bgneal@1: self.create_size(photosize) bgneal@1: bgneal@1: def remove_cache_dirs(self): bgneal@1: try: bgneal@1: os.removedirs(self.cache_path()) bgneal@1: except: bgneal@1: pass bgneal@1: bgneal@1: def save(self, update=False): bgneal@1: if update: bgneal@1: models.Model.save(self) bgneal@1: return bgneal@1: if self.date_taken is None: bgneal@1: try: bgneal@1: exif_date = self.EXIF.get('EXIF DateTimeOriginal', None) bgneal@1: if exif_date is not None: bgneal@1: d, t = str.split(exif_date.values) bgneal@1: year, month, day = d.split(':') bgneal@1: hour, minute, second = t.split(':') bgneal@1: self.date_taken = datetime(int(year), int(month), int(day), bgneal@1: int(hour), int(minute), int(second)) bgneal@1: except: bgneal@1: pass bgneal@1: if self.date_taken is None: bgneal@1: self.date_taken = datetime.now() bgneal@1: if self._get_pk_val(): bgneal@1: self.clear_cache() bgneal@1: super(ImageModel, self).save() bgneal@1: self.pre_cache() bgneal@1: bgneal@1: def delete(self): bgneal@1: self.clear_cache() bgneal@1: super(ImageModel, self).delete() bgneal@1: bgneal@1: bgneal@1: class Photo(ImageModel): bgneal@1: title = models.CharField(_('title'), max_length=100, unique=True) bgneal@1: title_slug = models.SlugField(_('slug'), unique=True, bgneal@1: help_text=('A "slug" is a unique URL-friendly title for an object.')) bgneal@1: caption = models.TextField(_('caption'), blank=True) bgneal@1: date_added = models.DateTimeField(_('date added'), default=datetime.now, editable=False) bgneal@1: is_public = models.BooleanField(_('is public'), default=True, help_text=_('Public photographs will be displayed in the default views.')) bgneal@1: tags = TagField(help_text=tagfield_help_text, verbose_name=_('tags')) bgneal@1: bgneal@1: class Meta: bgneal@1: ordering = ['-date_added'] bgneal@1: get_latest_by = 'date_added' bgneal@1: verbose_name = _("photo") bgneal@1: verbose_name_plural = _("photos") bgneal@1: bgneal@1: def __unicode__(self): bgneal@1: return self.title bgneal@1: bgneal@1: def __str__(self): bgneal@1: return self.__unicode__() bgneal@1: bgneal@1: def save(self, update=False): bgneal@1: if self.title_slug is None: bgneal@1: self.title_slug = slugify(self.title) bgneal@1: super(Photo, self).save(update) bgneal@1: bgneal@1: def get_absolute_url(self): bgneal@1: return reverse('pl-photo', args=[self.title_slug]) bgneal@1: bgneal@1: def public_galleries(self): bgneal@1: """Return the public galleries to which this photo belongs.""" bgneal@1: return self.galleries.filter(is_public=True) bgneal@1: bgneal@1: bgneal@1: class BaseEffect(models.Model): bgneal@1: name = models.CharField(_('name'), max_length=30, unique=True) bgneal@1: description = models.TextField(_('description'), blank=True) bgneal@1: bgneal@1: class Meta: bgneal@1: abstract = True bgneal@1: bgneal@1: def sample_dir(self): bgneal@1: return os.path.join(settings.MEDIA_ROOT, PHOTOLOGUE_DIR, 'samples') bgneal@1: bgneal@1: def sample_url(self): bgneal@1: return settings.MEDIA_URL + '/'.join([PHOTOLOGUE_DIR, 'samples', '%s %s.jpg' % (self.name.lower(), 'sample')]) bgneal@1: bgneal@1: def sample_filename(self): bgneal@1: return os.path.join(self.sample_dir(), '%s %s.jpg' % (self.name.lower(), 'sample')) bgneal@1: bgneal@1: def create_sample(self): bgneal@1: if not os.path.isdir(self.sample_dir()): bgneal@1: os.makedirs(self.sample_dir()) bgneal@1: try: bgneal@1: im = Image.open(SAMPLE_IMAGE_PATH) bgneal@1: except IOError: bgneal@1: raise IOError('Photologue was unable to open the sample image: %s.' % SAMPLE_IMAGE_PATH) bgneal@1: im = self.process(im) bgneal@1: im.save(self.sample_filename(), 'JPEG', quality=90, optimize=True) bgneal@1: bgneal@1: def admin_sample(self): bgneal@1: return u'' % self.sample_url() bgneal@1: admin_sample.short_description = 'Sample' bgneal@1: admin_sample.allow_tags = True bgneal@1: bgneal@1: def pre_process(self, im): bgneal@1: return im bgneal@1: bgneal@1: def post_process(self, im): bgneal@1: return im bgneal@1: bgneal@1: def process(self, im): bgneal@1: im = self.pre_process(im) bgneal@1: im = self.post_process(im) bgneal@1: return im bgneal@1: bgneal@1: def __unicode__(self): bgneal@1: return self.name bgneal@1: bgneal@1: def __str__(self): bgneal@1: return self.__unicode__() bgneal@1: bgneal@1: def save(self): bgneal@1: try: bgneal@1: os.remove(self.sample_filename()) bgneal@1: except: bgneal@1: pass bgneal@1: models.Model.save(self) bgneal@1: self.create_sample() bgneal@1: for size in self.photo_sizes.all(): bgneal@1: size.clear_cache() bgneal@1: # try to clear all related subclasses of ImageModel bgneal@1: for prop in [prop for prop in dir(self) if prop[-8:] == '_related']: bgneal@1: for obj in getattr(self, prop).all(): bgneal@1: obj.clear_cache() bgneal@1: obj.pre_cache() bgneal@1: bgneal@1: def delete(self): bgneal@1: try: bgneal@1: os.remove(self.sample_filename()) bgneal@1: except: bgneal@1: pass bgneal@1: super(PhotoEffect, self).delete() bgneal@1: bgneal@1: bgneal@1: class PhotoEffect(BaseEffect): bgneal@1: """ A pre-defined effect to apply to photos """ bgneal@1: transpose_method = models.CharField(_('rotate or flip'), max_length=15, blank=True, choices=IMAGE_TRANSPOSE_CHOICES) bgneal@1: 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.")) bgneal@1: 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.")) bgneal@1: 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.")) bgneal@1: 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.")) bgneal@1: filters = models.CharField(_('filters'), max_length=200, blank=True, help_text=_(IMAGE_FILTERS_HELP_TEXT)) bgneal@1: 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.")) bgneal@1: reflection_strength = models.FloatField(_('strength'), default=0.6, help_text="The initial opacity of the reflection gradient.") bgneal@1: 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.") bgneal@1: bgneal@1: class Meta: bgneal@1: verbose_name = _("photo effect") bgneal@1: verbose_name_plural = _("photo effects") bgneal@1: bgneal@1: def pre_process(self, im): bgneal@1: if self.transpose_method != '': bgneal@1: method = getattr(Image, self.transpose_method) bgneal@1: im = im.transpose(method) bgneal@1: if im.mode != 'RGB' and im.mode != 'RGBA': bgneal@1: return im bgneal@1: for name in ['Color', 'Brightness', 'Contrast', 'Sharpness']: bgneal@1: factor = getattr(self, name.lower()) bgneal@1: if factor != 1.0: bgneal@1: im = getattr(ImageEnhance, name)(im).enhance(factor) bgneal@1: for name in self.filters.split('->'): bgneal@1: image_filter = getattr(ImageFilter, name.upper(), None) bgneal@1: if image_filter is not None: bgneal@1: try: bgneal@1: im = im.filter(image_filter) bgneal@1: except ValueError: bgneal@1: pass bgneal@1: return im bgneal@1: bgneal@1: def post_process(self, im): bgneal@1: if self.reflection_size != 0.0: bgneal@1: im = add_reflection(im, bgcolor=self.background_color, amount=self.reflection_size, opacity=self.reflection_strength) bgneal@1: return im bgneal@1: bgneal@1: bgneal@1: class Watermark(BaseEffect): bgneal@1: image = models.ImageField(_('image'), upload_to=PHOTOLOGUE_DIR+"/watermarks") bgneal@1: style = models.CharField(_('style'), max_length=5, choices=WATERMARK_STYLE_CHOICES, default='scale') bgneal@1: opacity = models.FloatField(_('opacity'), default=1, help_text=_("The opacity of the overlay.")) bgneal@1: bgneal@1: class Meta: bgneal@1: verbose_name = _('watermark') bgneal@1: verbose_name_plural = _('watermarks') bgneal@1: bgneal@1: def post_process(self, im): bgneal@1: mark = Image.open(self.image.path) bgneal@1: return apply_watermark(im, mark, self.style, self.opacity) bgneal@1: bgneal@1: bgneal@1: class PhotoSize(models.Model): bgneal@1: 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".')) bgneal@1: width = models.PositiveIntegerField(_('width'), default=0, help_text=_('If width is set to "0" the image will be scaled to the supplied height.')) bgneal@1: height = models.PositiveIntegerField(_('height'), default=0, help_text=_('If height is set to "0" the image will be scaled to the supplied width')) bgneal@1: quality = models.PositiveIntegerField(_('quality'), choices=JPEG_QUALITY_CHOICES, default=70, help_text=_('JPEG image quality.')) bgneal@1: 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.')) bgneal@1: crop = models.BooleanField(_('crop to fit?'), default=False, help_text=_('If selected the image will be scaled and cropped to fit the supplied dimensions.')) bgneal@1: pre_cache = models.BooleanField(_('pre-cache?'), default=False, help_text=_('If selected this photo size will be pre-cached as photos are added.')) bgneal@1: 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.')) bgneal@1: effect = models.ForeignKey('PhotoEffect', null=True, blank=True, related_name='photo_sizes', verbose_name=_('photo effect')) bgneal@1: watermark = models.ForeignKey('Watermark', null=True, blank=True, related_name='photo_sizes', verbose_name=_('watermark image')) bgneal@1: bgneal@1: class Meta: bgneal@1: ordering = ['width', 'height'] bgneal@1: verbose_name = _('photo size') bgneal@1: verbose_name_plural = _('photo sizes') bgneal@1: bgneal@1: def __unicode__(self): bgneal@1: return self.name bgneal@1: bgneal@1: def __str__(self): bgneal@1: return self.__unicode__() bgneal@1: bgneal@1: def clear_cache(self): bgneal@1: for cls in ImageModel.__subclasses__(): bgneal@1: for obj in cls.objects.all(): bgneal@1: obj.remove_size(self) bgneal@1: if self.pre_cache: bgneal@1: obj.create_size(self) bgneal@1: PhotoSizeCache().reset() bgneal@1: bgneal@1: def save(self): bgneal@1: if self.width + self.height <= 0: bgneal@1: raise ValueError(_('A PhotoSize must have a positive height or width.')) bgneal@1: super(PhotoSize, self).save() bgneal@1: PhotoSizeCache().reset() bgneal@1: self.clear_cache() bgneal@1: bgneal@1: def delete(self): bgneal@1: self.clear_cache() bgneal@1: super(PhotoSize, self).delete() bgneal@1: bgneal@1: def _get_size(self): bgneal@1: return (self.width, self.height) bgneal@1: def _set_size(self, value): bgneal@1: self.width, self.height = value bgneal@1: size = property(_get_size, _set_size) bgneal@1: bgneal@1: bgneal@1: class PhotoSizeCache(object): bgneal@1: __state = {"sizes": {}} bgneal@1: bgneal@1: def __init__(self): bgneal@1: self.__dict__ = self.__state bgneal@1: if not len(self.sizes): bgneal@1: sizes = PhotoSize.objects.all() bgneal@1: for size in sizes: bgneal@1: self.sizes[size.name] = size bgneal@1: bgneal@1: def reset(self): bgneal@1: self.sizes = {} bgneal@1: bgneal@1: bgneal@1: # Set up the accessor methods bgneal@1: def add_methods(sender, instance, signal, *args, **kwargs): bgneal@1: """ Adds methods to access sized images (urls, paths) bgneal@1: bgneal@1: after the Photo model's __init__ function completes, bgneal@1: this method calls "add_accessor_methods" on each instance. bgneal@1: """ bgneal@1: if hasattr(instance, 'add_accessor_methods'): bgneal@1: instance.add_accessor_methods() bgneal@1: bgneal@1: # connect the add_accessor_methods function to the post_init signal bgneal@1: post_init.connect(add_methods)