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)