from django.db.models.fields.files import ImageField, ImageFieldFile from django.core.files.base import File, ContentFile class ImageVariation(File): variation_suffix = 'variation' def __init__(self, instance): self.field = instance.field self.instance = instance.instance self.storage = getattr(self, 'storage', instance.storage) name = getattr(self.instance, self.field.name) or u'' self._name = self.get_variation_name(unicode(name)) self._closed = False def get_variation_name(self, name): try: dot_index = name.rindex('.') except: name = '%s%s' % (name, self.variation_suffix) else: name = '%s%s%s' % (name[:dot_index], self.variation_suffix, name[dot_index:]) return name def save(self, name, content): self.storage.save(self.name, content) def delete(self): self.close() self.storage.delete(self.name) def _require_file(self): if not self: raise ValueError("The '%s' attribute has no file associated with it." % self.field.name) def _get_file(self): self._require_file() if not hasattr(self, '_file'): self._file = self.storage.open(self.name, 'rb') return self._file file = property(_get_file) def _get_path(self): self._require_file() return self.storage.path(self.name) path = property(_get_path) def _get_url(self): self._require_file() return self.storage.url(self.name) url = property(_get_url) def open(self, mode='rb'): self._require_file() return super(ImageVariation, self).open(mode) # open() doesn't alter the file's contents, but it does reset the pointer open.alters_data = True from StringIO import StringIO def get_file(data): if hasattr(data, 'temporary_file_path'): file = data.temporary_file_path() else: if hasattr(data, 'read'): file = StringIO(data.read()) else: file = StringIO(data['content']) return file WIDTH, HEIGHT = 0, 1 def resize_image(content, size): from PIL import Image img = Image.open(get_file(content)) if img.size[0] > size['width'] or img.size[1] > size['height']: if size['force']: target_height = float(size['height'] * img.size[WIDTH]) / size['width'] if target_height < img.size[HEIGHT]: # Crop height crop_side_size = int((img.size[HEIGHT] - target_height) / 2) img = img.crop((0, crop_side_size, img.size[WIDTH], img.size[HEIGHT] - crop_side_size)) elif target_height > img.size[HEIGHT]: # Crop width target_width = float(size['width'] * img.size[HEIGHT]) / size['height'] crop_side_size = int((img.size[WIDTH] - target_width) / 2) img = img.crop((crop_side_size, 0, img.size[WIDTH] - crop_side_size, img.size[HEIGHT])) img.thumbnail((size['width'], size['height']), Image.ANTIALIAS) out = StringIO() try: img.save(out, optimize=1) except IOError: img.save(out) return out else: return content class Thumbnail(ImageVariation): variation_suffix = 'thumbnail' def save(self, name, content): self.storage.save(self.name, resize_image(content, {'height': 200, 'width': 200, 'force': True} ) ) class ImageVariationsDescriptor(object): def __get__(self, instance=None, owner=None): self.variations = [ (v(instance)) for v in instance.field.variations ] for v in self.variations: setattr(self, v.__class__.__name__.lower(), v) return self def save(self, name, content): for v in self.variations: content.seek(0) v.save(name, content) def delete(self): for v in self.variations: v.delete() class ImageVariationsFieldFile(ImageFieldFile): variations = ImageVariationsDescriptor() def save(self, name, content, save=True): super(ImageVariationsFieldFile, self).save(name, content, save=save) self.variations.save(name, content) def delete(self, save=True): self.variations.delete() super(ImageVariationsFieldFile, self).delete(save) class ImageVariationsField(ImageField): attr_class = ImageVariationsFieldFile def __init__(self, variations=[], *args, **kwargs): self.variations = variations super(ImageVariationsField, self).__init__(*args, **kwargs)