12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355 |
- 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'<a href="%s"><img src="%s"></a>' % (
- self.get_absolute_url(),
- func(),
- )
- else:
- return u'<a href="%s"><img src="%s"></a>' % (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'<img src="%s">' % 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)
|