import os import random import shutil import zipfile from datetime import datetime from inspect import isclass from django.db import models from django.db.models.signals import post_init from django.conf import settings from django.core.files.base import ContentFile from django.core.urlresolvers import reverse from django.template.defaultfilters import slugify from django.utils.functional import curry from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User from django.contrib.sites.managers import CurrentSiteManager from django.contrib.sites.models import Site from django_extensions.db.models import TimeStampedModel from django_extensions.db.fields import AutoSlugField from darkroom.managers import * from directory.models import Town, Place from taggit.managers import TaggableManager from taggit.models import Tag # Required PIL classes may or may not be available from the root namespace # depending on the installation method used. try: import Image import ImageFile import ImageFilter import ImageEnhance except ImportError: try: from PIL import Image from PIL import ImageFile from PIL import ImageFilter from PIL import ImageEnhance except ImportError: raise ImportError( _( "Photologue was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path." ) ) from utils import EXIF from utils.image_entropy import image_entropy from utils.reflection import add_reflection from utils.watermark import apply_watermark # Path to sample image SAMPLE_IMAGE_PATH = getattr( settings, "SAMPLE_IMAGE_PATH", os.path.join(os.path.dirname(__file__), "res", "sample.jpg"), ) # os.path.join(settings.PROJECT_PATH, 'darkroom', 'res', 'sample.jpg' # Modify image file buffer size. ImageFile.MAXBLOCK = getattr(settings, "DARKROOM_MAXBLOCK", 256 * 2 ** 10) # Photologue image path relative to media root DARKROOM_DIR = getattr(settings, "DARKROOM_DIR", "darkroom/") # Look for user function to define file paths DARKROOM_PATH = getattr(settings, "DARKROOM_PATH", None) if DARKROOM_PATH is not None: if callable(DARKROOM_PATH): get_storage_path = DARKROOM_PATH else: parts = DARKROOM_PATH.split(".") module_name = ".".join(parts[:-1]) module = __import__(module_name) get_storage_path = getattr(module, parts[-1]) else: def get_storage_path(instance, filename): return os.path.join(DARKROOM_DIR, "photos", filename) # Quality options for JPEG images JPEG_QUALITY_CHOICES = ( (30, _("Very Low")), (40, _("Low")), (50, _("Medium-Low")), (60, _("Medium")), (70, _("Medium-High")), (80, _("High")), (90, _("Very High")), (100, _("Unchanged")), ) # choices for new crop_anchor field in Photo CROP_ANCHOR_CHOICES = ( ("top", _("Top")), ("right", _("Right")), ("bottom", _("Bottom")), ("left", _("Left")), ("center", _("Center")), ("smart", _("Smart (Default)")), ) IMAGE_TRANSPOSE_CHOICES = ( ("FLIP_LEFT_RIGHT", _("Flip left to right")), ("FLIP_TOP_BOTTOM", _("Flip top to bottom")), ("ROTATE_90", _("Rotate 90 degrees counter-clockwise")), ("ROTATE_270", _("Rotate 90 degrees clockwise")), ("ROTATE_180", _("Rotate 180 degrees")), ) WATERMARK_STYLE_CHOICES = ( ("tile", _("Tile")), ("scale", _("Scale")), ) IMAGE_ORIENTATION_CHOICES = ( (0, _("no rotation")), (2, _("Rotate 90 degrees counter-clockwise")), (3, _("Rotate 180 degrees")), (4, _("Rotate 90 degrees clockwise")), ) # see http://www.impulseadventure.com/photo/exif-orientation.html IMAGE_EXIF_ORIENTATION_MAP = { 1: 0, 8: 2, 3: 3, 6: 4, } # Slideshow size choices, as dictated by the Soundslides application # default ouputs. SLIDESHOW_SIZE_CHOICES = ( ("425x356", _("Small")), ("620x533", _("Standard")), ("800x596", _("Large")), ) def _compare_entropy(start_slice, end_slice, slice, difference): """ Calculate the entropy of two slices (from the start and end of an axis), returning a tuple containing the amount that should be added to the start and removed from the end of the axis. """ start_entropy = image_entropy(start_slice) end_entropy = image_entropy(end_slice) if end_entropy and abs(start_entropy / end_entropy - 1) < 0.01: # Less than 1% difference, remove from both sides. if difference >= slice * 2: return slice, slice half_slice = slice // 2 return half_slice, slice - half_slice if start_entropy > end_entropy: return 0, slice else: return slice, 0 # Prepare a list of image filters filter_names = [] for n in dir(ImageFilter): klass = getattr(ImageFilter, n) if ( isclass(klass) and issubclass(klass, ImageFilter.BuiltinFilter) and hasattr(klass, "name") ): filter_names.append(klass.__name__) 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)) ) # Next five functions streamline expanding zip files to folders def path_name(name): return os.path.split(name)[0] def file_name(name): return os.path.split(name)[1] def path_names(names): return (name for name in names if path_name(name) and not file_name(name)) def file_names(names): return (name for name in names if file_name(name)) def extract(zipfile): SLIDE_PATH = settings.MEDIA_ROOT + DARKROOM_DIR + "slideshows/" names = zipfile.namelist() for name in path_names(names): if not os.path.exists(SLIDE_PATH + name): if not name.find("__MACOSX"): # do not process meta or hidden files continue else: os.mkdir(SLIDE_PATH + name) for name in file_names(names): if name.startswith("__MACOSX"): # do not process meta or hidden files continue else: outfile = file(SLIDE_PATH + name, "wb") outfile.write(zipfile.read(name)) outfile.close() return names[0].split("/")[0] # Return the root folder for the model's sake # End zip file to folder functions class MediaMetadata(TimeStampedModel): title = models.CharField(_("title"), max_length=100) slug = AutoSlugField(_("slug"), populate_from="title", editable=True) description = models.TextField(_("description"), blank=True, null=True) published = models.BooleanField( _("published"), default=False, help_text=_("Published items will appear on all parts of the site."), ) published_on = models.DateTimeField(_("date published"), blank=True) weight = models.IntegerField(_("Weight"), blank=True, null=True, max_length=2) towns = models.ManyToManyField(Town, blank=True, null=True) tags = TaggableManager(blank=True) auto_tag = models.BooleanField(_("auto-tag"), default=True) class Meta: unique_together = (("title", "slug"),) abstract = True def do_auto_tag(self): """ Performs the auto-tagging work if necessary. Returns True if an additional save is required, False otherwise. """ found = False if self.auto_tag: import re # don't clobber any existing tags! try: existing_ids = [t.id for t in self.tags.all()] unused = Tag.objects.all() unused = unused.exclude(id__in=existing_ids) except: unused = Tag.objects.all() for tag in unused: regex = re.compile(r"\b%s\b" % tag.name, re.I) if regex.search(self.description) or regex.search(self.title): self.tags.add(tag) found = True return found def save(self, *args, **kwargs): super(TimeStampedModel, self).save(*args, **kwargs) requires_save = self.do_auto_tag() if requires_save: super(TimeStampedModel, self).save() class Photographer(TimeStampedModel): name = models.CharField(_("name"), max_length=100) title = models.CharField(_("Title"), blank=True, null=True, max_length=100) slug = AutoSlugField(_("slug"), populate_from="name", editable=True) user = models.ForeignKey(User, blank=True, null=True) date_added = models.DateTimeField( _("date added"), default=datetime.now(), editable=False ) class Meta: ordering = ("name",) get_latest_by = "date_added" def __unicode__(self): return self.name def __str__(self): return self.__unicode__() def get_absolute_url(self): return reverse("dr-photographer", args=[self.slug]) class Slideshow(MediaMetadata): preview = models.ImageField( _("preview"), upload_to=get_storage_path, help_text="Slideshow previews are a standard 410 pixels wide by 276 pixels high", ) folder = models.CharField(_("folder"), max_length=200, null=True, blank=True) size = models.CharField( _("size"), max_length=9, default="620x533", choices=SLIDESHOW_SIZE_CHOICES ) class Meta: ordering = ["created"] get_latest_by = "created" verbose_name = _("slideshow") verbose_name_plural = _("slideshows") def __unicode__(self): return self.title def __str__(self): return self.__unicode__() def get_absolute_url(self): return reverse("dr-slideshow-detail", args=[self.slug]) def path(self): return def delete(self): try: os.remove(self.folder) except: pass super(Slideshow, self).delete() class SlideshowUpload(models.Model): zip_file = models.FileField( _("slideshow file (.zip)"), upload_to=DARKROOM_DIR + "/slideshows", help_text=_("Select a .zip file with files to create a new Slideshow."), ) slideshow = models.ForeignKey(Slideshow, null=True, blank=True) class Meta: verbose_name = _("slideshow upload") verbose_name_plural = _("slideshow uploads") def save(self, *args, **kwargs): super(SlideshowUpload, self).save(*args, **kwargs) self.process_zipfile() super(SlideshowUpload, self).delete() def process_zipfile(self): if os.path.isfile(self.zip_file.path): # TODO: implement try-except here zip = zipfile.ZipFile(self.zip_file.path) bad_file = zip.testzip() if bad_file: raise Exception('"%s" in the .zip archive is corrupt.' % bad_file) folder = extract(zip) slideshow = self.slideshow slideshow.folder = extract(zip) # Extract the zip contents, # save the folder name to the model slideshow.save() zip.close() class Webcam(models.Model): title = models.CharField(_("title"), max_length=80) slug = AutoSlugField(_("slug"), populate_from="title", editable=True) town = models.ForeignKey(Town, blank=True, null=True) image_file = models.CharField(_("webcam image location"), max_length=255) thumb_file = models.CharField( _("webcam thumbnail location"), max_length=255, blank=True, null=True ) image_width = models.IntegerField( _("image width"), max_length=5, blank=True, null=True ) image_height = models.IntegerField( _("image height"), max_length=5, blank=True, null=True ) alt_text = models.CharField(_("alt text"), max_length=255, blank=True, null=True) description = models.TextField(_("description"), blank=True, null=True) ordering = models.IntegerField(_("order"), blank=True, null=True, max_length=3) class Meta: verbose_name = _("webcam") verbose_name_plural = _("webcams") ordering = ["ordering"] def __unicode__(self): return u"%s webcam" % self.title def get_absolute_url(self): return reverse("dr-webcam-detail", args=[self.slug]) class Gallery(MediaMetadata): preview = models.ImageField( _("graphic"), upload_to=DARKROOM_DIR + "/gallery-graphics", help_text="Gallery previews are a displayed at the top of the page if they exist.", null=True, blank=True, ) photos = models.ManyToManyField( "Photo", related_name="gallery_photos", verbose_name=_("photos"), null=True, blank=True, ) slideshow = models.ForeignKey(Slideshow, null=True, blank=True) photographer = models.ForeignKey(Photographer, null=True, blank=True) sites = models.ManyToManyField(Site) objects = MediaManager() on_site = CurrentSiteManager("sites") class Meta: ordering = ["-created"] get_latest_by = "created" verbose_name = _("gallery") verbose_name_plural = _("galleries") def __unicode__(self): return self.title @models.permalink def get_absolute_url(self, site=""): return ( "dr-gallery-detail", (), { "year": self.published_on.year, "month": self.published_on.strftime("%b").lower(), "day": self.published_on.day, "slug": self.slug, }, ) def last_photo(self): return self.photos.latest() def latest(self, limit=0, published=True): if limit == 0: limit = self.photo_count() if published: return self.published()[:limit] else: return self.photos.all()[:limit] def sample(self, count=0, published=True): if count == 0 or count > self.photo_count(): count = self.photo_count() if published: photo_set = self.published() else: photo_set = self.photos.all() return random.sample(photo_set, count) def photo_count(self, published=True): if published: return self.published_photos().count() else: return self.photos.all().count() photo_count.short_description = _("count") def published_photos(self): return self.photos.filter(published=True).order_by("created") def save(self, *args, **kwargs): super(Gallery, self).save(*args, **kwargs) if self.photos: if self.published: for p in self.photos.all(): p.published = True p.save() else: for p in self.photos.all(): p.published = False p.save() class GalleryUpload(models.Model): zip_file = models.FileField( _("images file (.zip)"), upload_to=DARKROOM_DIR + "/temp", help_text=_("Select a .zip file of images to upload into a new Gallery."), ) 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." ), ) title = models.CharField( _("title"), blank=True, null=True, help_text=_("Used to create a gallery if none is specified."), max_length=140, ) description = models.TextField( _("description"), blank=True, help_text=_("Descrpition will be added to all photos."), ) photographer = models.ForeignKey(Photographer, null=True, blank=True) class Meta: verbose_name = _("gallery upload") verbose_name_plural = _("gallery uploads") def save(self): super(GalleryUpload, self).save(*args, **kwargs) self.process_zipfile() super(GalleryUpload, self).delete() def process_zipfile(self): if os.path.isfile(self.zip_file.path): # TODO: implement try-except here zip = zipfile.ZipFile(self.zip_file.path) bad_file = zip.testzip() if bad_file: raise Exception('"%s" in the .zip archive is corrupt.' % bad_file) count = 1 if self.gallery: gallery = self.gallery else: gallery = Gallery.objects.create( title=self.title, slug=slugify(self.title), description=self.description, photographer=self.photographer, ) from cStringIO import StringIO for filename in zip.namelist(): if filename.startswith("__"): # do not process meta files continue data = zip.read(filename) if len(data): try: # the following is taken from django.newforms.fields.ImageField: # load() is the only method that can spot a truncated JPEG, # but it cannot be called sanely after verify() trial_image = Image.open(StringIO(data)) trial_image.load() # verify() is the only method that can spot a corrupt PNG, # but it must be called immediately after the constructor trial_image = Image.open(StringIO(data)) trial_image.verify() except Exception: # if a "bad" file is found we just skip it. continue while 1: title = " ".join([gallery.title, str(count)]) slug = slugify(title) try: p = Photo.objects.get(slug=slug) except Photo.DoesNotExist: photo = Photo( title=title, slug=slug, published_on=gallery.published_on, description=self.description, published=gallery.published, photographer=self.photographer, ) photo.image.save(filename, ContentFile(data)) # If EXIF data exists for comment and headline, set appropriate image info # if photo.EXIF['Image ImageDescription']: # photo.description=unicode(photo.EXIF['Image ImageDescription']) # else: # pass # if photo.EXIF['Image DocumentName']: # photo.title=photo.EXIFunicode(['Image DocumentName']) # photo.slug=slugify(photo.EXIF['Image DocumentName']) # else: # pass photo.save() gallery.photos.add(photo) count = count + 1 break count = count + 1 zip.close() class ImageModel(MediaMetadata): view_count = models.PositiveIntegerField(default=0, editable=False) crop_from = models.CharField( _("crop from"), blank=True, max_length=10, default="center", choices=CROP_ANCHOR_CHOICES, ) effect = models.ForeignKey( "PhotoEffect", null=True, blank=True, related_name="%(class)s_related", verbose_name=_("effect"), ) orientation = models.IntegerField( _("Orientation"), choices=IMAGE_ORIENTATION_CHOICES, null=True, blank=True ) class Meta: abstract = True @property def EXIF(self): try: return EXIF.process_file(open(self.image.path, "rb")) except: try: return EXIF.process_file(open(self.image.path, "rb"), details=False) except: return {} def admin_thumbnail(self): func = getattr(self, "get_admin_thumbnail_url", None) if func is None: return _('An "admin_thumbnail" photo size has not been defined.') else: if hasattr(self, "get_absolute_url"): return u'' % ( self.get_absolute_url(), func(), ) else: return u'' % (self.image.url, func()) admin_thumbnail.short_description = _("Thumbnail") admin_thumbnail.allow_tags = True def cache_path(self): return os.path.join(os.path.dirname(self.image.path), "cache") def cache_url(self): return "/".join([os.path.dirname(self.image.url), "cache"]) def image_filename(self): return os.path.basename(self.image.path) def _get_filename_for_size(self, size): size = getattr(size, "name", size) base, ext = os.path.splitext(self.image_filename()) return "".join([base, "_", size, ext]) def _get_SIZE_photosize(self, size): return PhotoSizeCache().sizes.get(size) def _get_SIZE_size(self, size): photosize = PhotoSizeCache().sizes.get(size) if not self.size_exists(photosize): self.create_size(photosize) return Image.open(self._get_SIZE_filename(size)).size def _get_SIZE_url(self, size): photosize = PhotoSizeCache().sizes.get(size) if not self.size_exists(photosize): self.create_size(photosize) if photosize.increment_count: self.view_count += 1 self.save(update=True) return "/".join([self.cache_url(), self._get_filename_for_size(photosize.name)]) def _get_SIZE_filename(self, size): photosize = PhotoSizeCache().sizes.get(size) return os.path.join( self.cache_path(), self._get_filename_for_size(photosize.name) ) def add_accessor_methods(self, *args, **kwargs): for size in PhotoSizeCache().sizes.keys(): setattr(self, "get_%s_size" % size, curry(self._get_SIZE_size, size=size)) setattr( self, "get_%s_photosize" % size, curry(self._get_SIZE_photosize, size=size), ) setattr(self, "get_%s_url" % size, curry(self._get_SIZE_url, size=size)) setattr( self, "get_%s_filename" % size, curry(self._get_SIZE_filename, size=size), ) def size_exists(self, photosize): func = getattr(self, "get_%s_filename" % photosize.name, None) if func is not None: if os.path.isfile(func()): return True return False def resize_image(self, im, photosize): cur_width, cur_height = im.size new_width, new_height = photosize.size if photosize.crop: ratio = max(float(new_width) / cur_width, float(new_height) / cur_height) x = cur_width * ratio y = cur_height * ratio xd = abs(new_width - x) yd = abs(new_height - y) x_diff = int(xd / 2) y_diff = int(yd / 2) if self.crop_from == "top": box = (int(x_diff), 0, int(x_diff + new_width), new_height) elif self.crop_from == "left": box = (0, int(y_diff), new_width, int(y_diff + new_height)) elif self.crop_from == "bottom": box = ( int(x_diff), int(yd), int(x_diff + new_width), int(y), ) # y - yd = new_height elif self.crop_from == "right": box = ( int(xd), int(y_diff), int(x), int(y_diff + new_height), ) # x - xd = new_width elif self.crop_from == "smart": left = top = 0 right, bottom = x, y while xd: slice = min(xd, max(xd // 5, 10)) start = im.crop((left, 0, left + slice, y)) end = im.crop((right - slice, 0, right, y)) add, remove = _compare_entropy(start, end, slice, xd) left += add right -= remove xd = xd - add - remove while yd: slice = min(yd, max(yd // 5, 10)) start = im.crop((0, top, x, top + slice)) end = im.crop((0, bottom - slice, x, bottom)) add, remove = _compare_entropy(start, end, slice, yd) top += add bottom -= remove yd = yd - add - remove box = (left, top, right, bottom) else: box = ( int(x_diff), int(y_diff), int(x_diff + new_width), int(y_diff + new_height), ) im = im.resize((int(x), int(y)), Image.ANTIALIAS).crop(box) else: if not new_width == 0 and not new_height == 0: ratio = min( float(new_width) / cur_width, float(new_height) / cur_height ) else: if new_width == 0: ratio = float(new_height) / cur_height else: ratio = float(new_width) / cur_width new_dimensions = ( int(round(cur_width * ratio)), int(round(cur_height * ratio)), ) if new_dimensions[0] > cur_width or new_dimensions[1] > cur_height: if not photosize.upscale: return im im = im.resize(new_dimensions, Image.ANTIALIAS) return im def create_size(self, photosize): if self.size_exists(photosize): return if not os.path.isdir(self.cache_path()): os.makedirs(self.cache_path()) try: im = Image.open(self.image.path) except IOError: return # Apply effect if found if self.effect is not None: im = self.effect.pre_process(im) elif photosize.effect is not None: im = photosize.effect.pre_process(im) # Resize/crop image if im.size != photosize.size: im = self.resize_image(im, photosize) # Apply watermark if found if photosize.watermark is not None: im = photosize.watermark.post_process(im) # Apply effect if found if self.effect is not None: im = self.effect.post_process(im) elif photosize.effect is not None: im = photosize.effect.post_process(im) if self.orientation is not None and self.orientation > 0: im = im.transpose(self.orientation) # Save file im_filename = getattr(self, "get_%s_filename" % photosize.name)() try: if im.format == "JPEG": im.save( im_filename, "JPEG", quality=int(photosize.quality), optimize=True ) else: im.save(im_filename) except IOError, e: if os.path.isfile(im_filename): os.unlink(im_filename) raise e def remove_size(self, photosize, remove_dirs=True): if not self.size_exists(photosize): return filename = getattr(self, "get_%s_filename" % photosize.name)() if os.path.isfile(filename): os.remove(filename) if remove_dirs: self.remove_cache_dirs() def clear_cache(self): cache = PhotoSizeCache() for photosize in cache.sizes.values(): self.remove_size(photosize, False) self.remove_cache_dirs() def pre_cache(self): cache = PhotoSizeCache() for photosize in cache.sizes.values(): if photosize.pre_cache: self.create_size(photosize) def remove_cache_dirs(self): try: os.removedirs(self.cache_path()) except: pass def save(self, update=False, *args, **kwargs): if update: models.Model.save(self) return exif_title = self.EXIF.get("Image DocumentName", None) exif_description = self.EXIF.get("Image UserComment", None) if exif_title is not None: self.title = exif_title self.slug = slugify(exif_title) if exif_description is not None: self.description = exif_description if self.date_taken: if self.date_taken is None: try: exif_date = self.EXIF.get("EXIF DateTimeOriginal", None) if exif_date is not None: d, t = str.split(exif_date.values) year, month, day = d.split(":") hour, minute, second = t.split(":") self.date_taken = datetime( int(year), int(month), int(day), int(hour), int(minute), int(second), ) except: self.date_taken = datetime.now() if self.orientation is None: try: exif_orientation = self.EXIF.get("Image Orientation", 1).values[0] self.orientation = IMAGE_EXIF_ORIENTATION_MAP[exif_orientation] except: self.orientation = 0 if self._get_pk_val(): self.clear_cache() super(ImageModel, self).save(*args, **kwargs) self.pre_cache() def delete(self): self.clear_cache() super(ImageModel, self).delete() class Movie(MediaMetadata): flv_video = models.FileField( _("video file (.flv)"), upload_to=DARKROOM_DIR + "/movies" ) original_video = models.FileField( _("original file"), upload_to=DARKROOM_DIR + "/movies", blank=True, null=True, help_text=_("The original video preferably in a standard format like H.264."), ) flash_skin = models.CharField( _("flash player skin"), blank=False, null=True, max_length=100, help_text=_("Do not include the .swf extension with the skin name."), ) photographer = models.ForeignKey(Photographer, blank=True, null=True) preview = models.ImageField( _("preview"), upload_to=get_storage_path, help_text="Movie previews are a standard 70 pixels wide by 50 pixels high", ) objects = MediaManager() class Meta: ordering = ["-created"] get_latest_by = "created" verbose_name = _("movie") verbose_name_plural = _("movies") def __unicode__(self): return self.title def __str__(self): return self.__unicode__() def get_absolute_url(self): return reverse("dr-movie-detail", args=[self.slug]) class Photo(ImageModel): image = models.ImageField(_("Image"), upload_to=get_storage_path) cropped_image = models.ImageField( _("Cropped image"), blank=True, null=True, upload_to="darkroom/cropped/" ) date_taken = models.DateTimeField( _("Date taken"), null=True, blank=True, editable=False ) photographer = models.ForeignKey(Photographer, blank=True, null=True) courtesy = models.CharField(_("Courtesy"), max_length=140, blank=True, null=True) file_photo = models.BooleanField(_("File photo?"), default=False) sites = models.ManyToManyField(Site) objects = MediaManager() on_site = CurrentSiteManager("sites") class Meta: ordering = ["-created"] get_latest_by = "published_on" verbose_name = _("photo") verbose_name_plural = _("photos") def __unicode__(self): return self.title def save(self, update=False): if self.slug is None: self.slug = slugify(self.title) super(Photo, self).save(update) @models.permalink def get_absolute_url(self, site=""): return ( "dr-photo-detail", (), { "year": self.published_on.year, "month": self.published_on.strftime("%b").lower(), "day": self.published_on.day, "slug": self.slug, }, ) def get_lb_url(self): return reverse("dr-photo-lightbox", args=[self.slug]) def published_galleries(self): """Return the published galleries to which this photo belongs.""" return self.galleries.filter(published=True) def get_previous_in_gallery(self, gallery): try: return self.get_previous_by_published_on(galleries__exact=gallery) except Photo.DoesNotExist: return None def get_next_in_gallery(self, gallery): try: return self.get_next_by_published_on(galleries__exact=gallery) except Photo.DoesNotExist: return None class Graphic(ImageModel): image = models.ImageField(_("image"), upload_to=DARKROOM_DIR + "/graphics") date_taken = models.DateTimeField( _("date created"), null=True, blank=True, editable=False ) data_source = models.CharField( _("data source"), blank=True, null=True, max_length=150 ) source = models.ForeignKey(Place, blank=True, null=True) class Meta: ordering = ["-created"] get_latest_by = "created" verbose_name = _("graphic") verbose_name_plural = _("graphics") def __unicode__(self): return self.title def __str__(self): return self.__unicode__() @models.permalink def get_absolute_url(self, site=""): return ( "dr-graphic-detail", (), { "year": self.published_on.year, "month": self.published_on.strftime("%b").lower(), "day": self.published_on.day, "slug": self.slug, }, ) class BaseEffect(models.Model): name = models.CharField(_("name"), max_length=30, unique=True) description = models.TextField(_("description"), blank=True) class Meta: abstract = True def sample_dir(self): return os.path.join(settings.MEDIA_ROOT, DARKROOM_DIR, "samples") def sample_url(self): return settings.MEDIA_URL + "/".join( [DARKROOM_DIR, "samples", "%s %s.jpg" % (self.name.lower(), "sample")] ) def sample_filename(self): return os.path.join( self.sample_dir(), "%s %s.jpg" % (self.name.lower(), "sample") ) def create_sample(self): if not os.path.isdir(self.sample_dir()): os.makedirs(self.sample_dir()) try: im = Image.open(SAMPLE_IMAGE_PATH) except IOError: raise IOError( "Photologue was unable to open the sample image: %s." % SAMPLE_IMAGE_PATH ) im = self.process(im) im.save(self.sample_filename(), "JPEG", quality=90, optimize=True) def admin_sample(self): return u'' % self.sample_url() admin_sample.short_description = "Sample" admin_sample.allow_tags = True def pre_process(self, im): return im def post_process(self, im): return im def process(self, im): im = self.pre_process(im) im = self.post_process(im) return im def __unicode__(self): return self.name def __str__(self): return self.__unicode__() def save(self): try: os.remove(self.sample_filename()) except: pass models.Model.save(self) self.create_sample() for size in self.photo_sizes.all(): size.clear_cache() # try to clear all related subclasses of ImageModel for prop in [prop for prop in dir(self) if prop[-8:] == "_related"]: for obj in getattr(self, prop).all(): obj.clear_cache() obj.pre_cache() def delete(self): try: os.remove(self.sample_filename()) except: pass super(PhotoEffect, self).delete() class PhotoEffect(BaseEffect): """ A pre-defined effect to apply to photos """ transpose_method = models.CharField( _("rotate or flip"), max_length=15, blank=True, choices=IMAGE_TRANSPOSE_CHOICES ) 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." ), ) 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." ), ) 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." ), ) 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." ), ) filters = models.CharField( _("filters"), max_length=200, blank=True, help_text=_(IMAGE_FILTERS_HELP_TEXT) ) 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." ), ) reflection_strength = models.FloatField( _("strength"), default=0.6, help_text="The initial opacity of the reflection gradient.", ) 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.", ) class Meta: verbose_name = _("photo effect") verbose_name_plural = _("photo effects") def pre_process(self, im): if self.transpose_method != "": method = getattr(Image, self.transpose_method) im = im.transpose(method) if im.mode != "RGB" and im.mode != "RGBA": return im for name in ["Color", "Brightness", "Contrast", "Sharpness"]: factor = getattr(self, name.lower()) if factor != 1.0: im = getattr(ImageEnhance, name)(im).enhance(factor) for name in self.filters.split("->"): image_filter = getattr(ImageFilter, name.upper(), None) if image_filter is not None: try: im = im.filter(image_filter) except ValueError: pass return im def post_process(self, im): if self.reflection_size != 0.0: im = add_reflection( im, bgcolor=self.background_color, amount=self.reflection_size, opacity=self.reflection_strength, ) return im class Watermark(BaseEffect): image = models.ImageField(_("image"), upload_to=DARKROOM_DIR + "/watermarks") style = models.CharField( _("style"), max_length=5, choices=WATERMARK_STYLE_CHOICES, default="scale" ) opacity = models.FloatField( _("opacity"), default=1, help_text=_("The opacity of the overlay.") ) class Meta: verbose_name = _("watermark") verbose_name_plural = _("watermarks") def post_process(self, im): mark = Image.open(self.image.path) return apply_watermark(im, mark, self.style, self.opacity) class PhotoSize(models.Model): 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".' ), ) width = models.PositiveIntegerField( _("width"), default=0, help_text=_( 'If width is set to "0" the image will be scaled to the supplied height.' ), ) height = models.PositiveIntegerField( _("height"), default=0, help_text=_( 'If height is set to "0" the image will be scaled to the supplied width' ), ) quality = models.PositiveIntegerField( _("quality"), choices=JPEG_QUALITY_CHOICES, default=70, help_text=_("JPEG image quality."), ) 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." ), ) crop = models.BooleanField( _("crop to fit?"), default=False, help_text=_( "If selected the image will be scaled and cropped to fit the supplied dimensions." ), ) pre_cache = models.BooleanField( _("pre-cache?"), default=False, help_text=_( "If selected this photo size will be pre-cached as photos are added." ), ) 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.' ), ) effect = models.ForeignKey( "PhotoEffect", null=True, blank=True, related_name="photo_sizes", verbose_name=_("photo effect"), ) watermark = models.ForeignKey( "Watermark", null=True, blank=True, related_name="photo_sizes", verbose_name=_("watermark image"), ) class Meta: ordering = ["width", "height"] verbose_name = _("photo size") verbose_name_plural = _("photo sizes") def __unicode__(self): return self.name def __str__(self): return self.__unicode__() def clear_cache(self): for cls in ImageModel.__subclasses__(): for obj in cls.objects.all(): obj.remove_size(self) if self.pre_cache: obj.create_size(self) PhotoSizeCache().reset() def save(self, *args, **kwargs): if self.width + self.height <= 0: raise ValueError(_("A PhotoSize must have a positive height or width.")) super(PhotoSize, self).save(*args, **kwargs) PhotoSizeCache().reset() self.clear_cache() def delete(self): self.clear_cache() super(PhotoSize, self).delete() def _get_size(self): return (self.width, self.height) def _set_size(self, value): self.width, self.height = value size = property(_get_size, _set_size) class PhotoSizeCache(object): __state = {"sizes": {}} def __init__(self): self.__dict__ = self.__state if not len(self.sizes): sizes = PhotoSize.objects.all() for size in sizes: self.sizes[size.name] = size def reset(self): self.sizes = {} # Set up the accessor methods def add_methods(sender, instance, signal, *args, **kwargs): """ Adds methods to access sized images (urls, paths) after the Photo model's __init__ function completes, this method calls "add_accessor_methods" on each instance. """ if hasattr(instance, "add_accessor_methods"): instance.add_accessor_methods() # connect the add_accessor_methods function to the post_init signal post_init.connect(add_methods)