django-imagekit/imagekit/fields.py
Matthew Tretter 3f7ca512af Extension argument to cache_to includes dot.
It seems that's how we do it in python world. Who am I to argue?
2011-09-23 12:44:53 -04:00

372 lines
13 KiB
Python
Executable file

import os
import datetime
from StringIO import StringIO
from imagekit.lib import *
from imagekit.utils import img_to_fobj, get_spec_files
from django.conf import settings
from django.core.files.base import ContentFile
from django.utils.encoding import force_unicode, smart_str
from django.db import models
from django.db.models.signals import post_save, post_delete
from django.utils.translation import ugettext_lazy as _
from django.template.loader import render_to_string
from django.db.models.fields.files import ImageFieldFile
# Modify image file buffer size.
ImageFile.MAXBLOCK = getattr(settings, 'PIL_IMAGEFILE_MAXBLOCK', 256 * 2 ** 10)
class _ImageSpecMixin(object):
def __init__(self, processors=None, quality=70, format=None):
self.processors = processors
self.quality = quality
self.format = format
def process(self, image, file):
fmt = image.format
img = image.copy()
for proc in self.processors:
img, fmt = proc.process(img, fmt, file)
format = self.format or fmt
img.format = format
return img, format
class ImageSpec(_ImageSpecMixin):
cache_to = None
"""Specifies the filename to use when saving the image cache file. This is
modeled after ImageField's `upload_to` and can accept either a string
(that specifies a directory) or a callable (that returns a filepath).
Callable values should accept the following arguments:
instance -- the model instance this spec belongs to
path -- the path of the original image
specname -- the property name that the spec is bound to on the model instance
extension -- a recommended extension. If the format of the spec is set
explicitly, this suggestion will be based on that format. if not,
the extension of the original file will be passed. You do not have
to use this extension, it's only a recommendation.
If you have not explicitly set a format on your ImageSpec, the extension of
the path returned by this function will be used to infer one.
"""
def __init__(self, processors=None, quality=70, format=None,
image_field=None, pre_cache=False, storage=None, cache_to=None):
_ImageSpecMixin.__init__(self, processors, quality=quality,
format=format)
self.image_field = image_field
self.pre_cache = pre_cache
self.storage = storage
self.cache_to = cache_to
def contribute_to_class(self, cls, name):
setattr(cls, name, _ImageSpecDescriptor(self, name))
# Connect to the signals only once for this class.
uid = '%s.%s' % (cls.__module__, cls.__name__)
post_save.connect(_post_save_handler,
sender=cls,
dispatch_uid='%s_save' % uid)
post_delete.connect(_post_delete_handler,
sender=cls,
dispatch_uid='%s.delete' % uid)
def _get_suggested_extension(name, format):
if format:
# Try to look up an extension by the format
extensions = [k for k, v in Image.EXTENSION.iteritems() \
if v == format.upper()]
else:
extensions = []
original_extension = os.path.splitext(name)[1]
if not extensions or original_extension.lower() in extensions:
# If the original extension matches the format, use it.
extension = original_extension
else:
extension = extensions[0]
return extension
class ImageSpecFile(object):
def __init__(self, instance, field, attname):
self.field = field
self._img = None
self._fmt = None
self.instance = instance
self.attname = attname
@property
def _format(self):
"""The format used to save the cache file. If the format is set
explicitly on the ImageSpec, that format will be used. Otherwise, the
format will be inferred from the extension of the cache filename (see
the `name` property).
"""
format = self.field.format
if not format:
# Get the real (not suggested) extension.
extension = os.path.splitext(self.name)[1].lower()
# Try to guess the format from the extension.
format = Image.EXTENSION.get(extension)
return format or self._img.format or 'JPEG'
def _get_imgfile(self):
format = self._format
if format != 'JPEG':
imgfile = img_to_fobj(self._img, format)
else:
imgfile = img_to_fobj(self._img, format,
quality=int(self.field.quality),
optimize=True)
return imgfile
@property
def _imgfield(self):
field_name = getattr(self.field, 'image_field', None)
if field_name:
field = getattr(self.instance, field_name)
else:
image_fields = [getattr(self.instance, f.attname) for f in \
self.instance.__class__._meta.fields if \
isinstance(f, models.ImageField)]
if len(image_fields) == 0:
raise Exception('{0} does not define any ImageFields, so your '
'{1} ImageSpec has no image to act on.'.format(
self.instance.__class__.__name__, self.attname))
elif len(image_fields) > 1:
raise Exception('{0} defines multiple ImageFields, but you have '
'not specified an image_field for your {1} '
'ImageSpec.'.format(self.instance.__class__.__name__,
self.attname))
else:
field = image_fields[0]
return field
def _create(self):
if self._imgfield:
# TODO: Should we error here or something if the image doesn't exist?
if self._exists():
return
# process the original image file
try:
fp = self._imgfield.storage.open(self._imgfield.name)
except IOError:
return
fp.seek(0)
fp = StringIO(fp.read())
self._img, self._fmt = self.field.process(Image.open(fp), self)
# save the new image to the cache
content = ContentFile(self._get_imgfile().read())
self.storage.save(self.name, content)
def _delete(self):
if self._imgfield:
try:
self.storage.delete(self.name)
except (NotImplementedError, IOError):
return
def _exists(self):
if self._imgfield:
return self.storage.exists(self.name)
@property
def _suggested_extension(self):
return _get_suggested_extension(self._imgfield.name, self.field.format)
def _default_cache_to(self, instance, path, specname, extension):
"""Determines the filename to use for the transformed image. Can be
overridden on a per-spec basis by setting the cache_to property on the
spec.
"""
filepath, basename = os.path.split(path)
filename = os.path.splitext(basename)[0]
new_name = '{0}_{1}{2}'.format(filename, specname, extension)
return os.path.join(os.path.join('cache', filepath), new_name)
@property
def name(self):
"""
Specifies the filename that the cached image will use. The user can
control this by providing a `cache_to` method to the ImageSpec.
"""
filename = self._imgfield.name
if filename:
cache_to = self.field.cache_to or self._default_cache_to
if not cache_to:
raise Exception('No cache_to or default_cache_to value specified')
if callable(cache_to):
new_filename = force_unicode(datetime.datetime.now().strftime( \
smart_str(cache_to(self.instance, self._imgfield.name, \
self.attname, self._suggested_extension))))
else:
dir_name = os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(cache_to))))
filename = os.path.normpath(os.path.basename(filename))
new_filename = os.path.join(dir_name, filename)
return new_filename
@property
def storage(self):
return self.field.storage or self._imgfield.storage
@property
def url(self):
if not self.field.pre_cache:
self._create()
return self.storage.url(self.name)
@property
def file(self):
self._create()
return self.storage.open(self.name)
@property
def image(self):
if not self._img:
self._create()
if not self._img:
self._img = Image.open(self.file)
return self._img
@property
def width(self):
return self.image.size[0]
@property
def height(self):
return self.image.size[1]
class _ImageSpecDescriptor(object):
def __init__(self, field, attname):
self._attname = attname
self.field = field
def __get__(self, instance, owner):
if instance is None:
return self.field
else:
return ImageSpecFile(instance, self.field, self._attname)
def _post_save_handler(sender, instance=None, created=False, raw=False, **kwargs):
if raw:
return
spec_files = get_spec_files(instance)
for spec_file in spec_files:
if not created:
spec_file._delete()
if spec_file.field.pre_cache:
spec_file._create()
def _post_delete_handler(sender, instance=None, **kwargs):
assert instance._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (instance._meta.object_name, instance._meta.pk.attname)
spec_files = get_spec_files(instance)
for spec_file in spec_files:
spec_file._delete()
class AdminThumbnailView(object):
short_description = _('Thumbnail')
allow_tags = True
def __init__(self, image_field, template=None):
"""
Keyword arguments:
image_field -- the name of the ImageField or ImageSpec on the model to
use for the thumbnail.
template -- the template with which to render the thumbnail
"""
self.image_field = image_field
self.template = template
def __get__(self, instance, owner):
if instance is None:
return self
else:
return BoundAdminThumbnailView(instance, self)
class BoundAdminThumbnailView(AdminThumbnailView):
def __init__(self, model_instance, unbound_field):
super(BoundAdminThumbnailView, self).__init__(unbound_field.image_field,
unbound_field.template)
self.model_instance = model_instance
def __unicode__(self):
thumbnail = getattr(self.model_instance, self.image_field, None)
if not thumbnail:
raise Exception('The property {0} is not defined on {1}.'.format(
self.model_instance, self.image_field))
original_image = getattr(thumbnail, '_imgfield', None) or thumbnail
template = self.template or 'imagekit/admin/thumbnail.html'
return render_to_string(template, {
'model': self.model_instance,
'thumbnail': thumbnail,
'original_image': original_image,
})
def __get__(self, instance, owner):
"""Override AdminThumbnailView's implementation."""
return self
class ProcessedImageFieldFile(ImageFieldFile):
def save(self, name, content, save=True):
new_filename = self.field.generate_filename(self.instance, name)
img = Image.open(content)
img, format = self.field.process(img, self)
format = self._get_format(new_filename, format)
if format != 'JPEG':
imgfile = img_to_fobj(img, format)
else:
imgfile = img_to_fobj(img, format,
quality=int(self.field.quality),
optimize=True)
content = ContentFile(imgfile.read())
return super(ProcessedImageFieldFile, self).save(name, content, save)
def _get_format(self, name, fallback):
format = self.field.format
if not format:
if callable(self.field.upload_to):
# The extension is explicit, so assume they want the matching format.
extension = os.path.splitext(name)[1].lower()
# Try to guess the format from the extension.
format = Image.EXTENSION.get(extension)
return format or fallback
class ProcessedImageField(models.ImageField, _ImageSpecMixin):
attr_class = ProcessedImageFieldFile
def __init__(self, processors=None, quality=70, format=None,
verbose_name=None, name=None, width_field=None, height_field=None,
**kwargs):
_ImageSpecMixin.__init__(self, processors, quality=quality,
format=format)
models.ImageField.__init__(self, verbose_name, name, width_field,
height_field, **kwargs)
def get_filename(self, filename):
filename = os.path.normpath(self.storage.get_valid_name(os.path.basename(filename)))
name, ext = os.path.splitext(filename)
ext = _get_suggested_extension(filename, self.format)
return '{0}.{1}'.format(name, ext)