Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

hemamaps / django-athumb   python

Repository URL to install this package:

Version: 2.4.1 

/ fields.py

# -*- encoding: utf-8 -*-
"""
Fields, FieldFiles, and Validators.
"""
import os
import cStringIO

from PIL import Image
from django.db.models import ImageField
from django.db.models.fields.files import ImageFieldFile
from django.conf import settings
from django.core.cache import cache
from django.core.files.base import ContentFile
from athumb.exceptions import UploadedImageIsUnreadableError
from athumb.pial.engines.pil_engine import PILEngine

from validators import ImageUploadExtensionValidator

try:
    #noinspection PyUnresolvedReferences
    from south.modelsinspector import add_introspection_rules
    add_introspection_rules([], ["^athumb\.fields\.ImageWithThumbsField"])
except ImportError:
    # Not using South, no big deal.
    pass

# Thumbnailing is done through here. Eventually we can support image libraries
# other than PIL.
THUMBNAIL_ENGINE = PILEngine()

# Cache URLs for thumbnails so we don't have to keep re-generating them.
THUMBNAIL_URL_CACHE_TIME = getattr(settings, 'THUMBNAIL_URL_CACHE_TIME', 3600 * 24)
# Optional cache-buster string to append to end of thumbnail URLs.
MEDIA_CACHE_BUSTER = getattr(settings, 'MEDIA_CACHE_BUSTER', '')

# Models want this instantiated ahead of time.
IMAGE_EXTENSION_VALIDATOR = ImageUploadExtensionValidator()

class ImageWithThumbsFieldFile(ImageFieldFile):
    """
    Serves as the file-level storage object for thumbnails.
    """
    def generate_url(self, thumb_name, ssl_mode=False, check_cache=True, cache_bust=True):
        # This is tacked on to the end of the cache key to make sure SSL
        # URLs are stored separate from plain http.
        ssl_postfix = '_ssl' if ssl_mode else ''

        # Try to see if we can hit the cache instead of asking the storage
        # backend for the URL. This is particularly important for S3 backends.

        cache_key = None

        if check_cache:
            cache_key = "Thumbcache_%s_%s%s" % (self.url,
                                                   thumb_name,
                                                   ssl_postfix)
            cache_key = cache_key.strip()

            cached_val = cache.get(cache_key)
            if cached_val:
                return cached_val

        # Determine what the filename would be for a thumb with these
        # dimensions, regardless of whether it actually exists.
        new_filename = self._calc_thumb_filename(thumb_name)

        # Split URL from GET attribs.
        url_get_split = self.url.rsplit('?', 1)
        # Just the URL string (no GET attribs).
        url_str = url_get_split[0]
        # Get the URL string without the original's filename at the end.
        url_minus_filename = url_str.rsplit('/', 1)[0]

        # Slap the new thumbnail filename on the end of the old URL, in place
        # of the orignal image's filename.
        new_url = "%s/%s" % (url_minus_filename,
                                      os.path.basename(new_filename))

        # Cache busters are a cheezy way to force some browsers to retrieve
        # an updated image, circumventing any long-term or infinite caching
        # you may have set when uploading to S3. This are entirely optional.
        if cache_bust and MEDIA_CACHE_BUSTER:
            new_url = "%s?cbust=%s" % (new_url, MEDIA_CACHE_BUSTER)

        if ssl_mode:
            new_url = new_url.replace('http://', 'https://')

        if cache_key:
            # Cache this so we don't have to hit the storage backend for a while.
            cache.set(cache_key, new_url, THUMBNAIL_URL_CACHE_TIME)

        return new_url

    def get_thumbnail_format(self):
        """
        Determines the target thumbnail type either by looking for a format
        override specified at the model level, or by using the format the
        user uploaded.
        """
        if self.field.thumbnail_format:
            # Over-ride was given, use that instead.
            return self.field.thumbnail_format.lower()
        else:
            # Use the existing extension from the file.
            filename_split = self.name.rsplit('.', 1)
            return filename_split[-1]

    def save(self, name, content, save=True):
        """
        Handles some extra logic to generate the thumbnails when the original
        file is uploaded.
        """
        super(ImageWithThumbsFieldFile, self).save(name, content, save)
        try:
            self.generate_thumbs(name, content)
        except IOError, exc:
            if 'cannot identify' in exc.message or \
               'bad EPS header' in exc.message:
                raise UploadedImageIsUnreadableError(
                    "We were unable to read the uploaded image. "
                    "Please make sure you are uploading a valid image file."
                )
            else:
                raise

    def generate_thumbs(self, name, content):
        # see http://code.djangoproject.com/ticket/8222 for details
        content.seek(0)
        image = Image.open(content)

        # Convert to RGBA (alpha) if necessary
        if image.mode not in ('L', 'RGB', 'RGBA'):
            image = image.convert('RGBA')

        for thumb in self.field.thumbs:
            thumb_name, thumb_options = thumb
            # Pre-create all of the thumbnail sizes.
            self.create_and_store_thumb(image, thumb_name, thumb_options)

    def _calc_thumb_filename(self, thumb_name):
        """
        Calculates the correct filename for a would-be (or potentially
        existing) thumbnail of the given size.
        
        NOTE: This includes the path leading up to the thumbnail. IE:
        uploads/cbid_images/photo.png
        
        size: (tuple) In the format of (width, height)
        
        Returns a string filename.
        """
        filename_split = self.name.rsplit('.', 1)
        file_name = filename_split[0]
        file_extension = self.get_thumbnail_format()

        return '%s_%s.%s' % (file_name, thumb_name, file_extension)

    def create_and_store_thumb(self, image, thumb_name, thumb_options):
        """
        Given that 'image' is a PIL Image object, create a thumbnail for the
        given size tuple and store it via the storage backend.
        
        image: (Image) PIL Image object.
        size: (tuple) Tuple in form of (width, height). Image will be
            thumbnailed to this size.
        """
        size = thumb_options['size']
        upscale = thumb_options.get('upscale', True)
        crop = thumb_options.get('crop')
        if crop is True:
            # We'll just make an assumption here. Center cropping is the
            # typical default.
            crop = 'center'

        thumb_filename = self._calc_thumb_filename(thumb_name)
        file_extension = self.get_thumbnail_format()

        # The work starts here.
        thumbed_image = THUMBNAIL_ENGINE.create_thumbnail(
            image,
            size,
            crop=crop,
            upscale=upscale
        )

        # TODO: Avoiding hitting the disk here, but perhaps we should use temp
        # files down the road? Big images might choke us as we do part in
        # RAM then hit swap.
        img_fobj = cStringIO.StringIO()
        # This writes the thumbnailed PIL.Image to the file-like object.
        THUMBNAIL_ENGINE.write(thumbed_image, img_fobj, format=file_extension)
        # Save the result to the storage backend.
        thumb_content = ContentFile(img_fobj.getvalue())
        self.storage.save(thumb_filename, thumb_content)
        img_fobj.close()

    def delete(self, save=True):
        """
        Deletes the original, plus any thumbnails. Fails silently if there
        are errors deleting the thumbnails.
        """
        for thumb in self.field.thumbs:
            thumb_name, thumb_options = thumb
            thumb_filename = self._calc_thumb_filename(thumb_name)
            self.storage.delete(thumb_filename)

        super(ImageWithThumbsFieldFile, self).delete(save)

class ImageWithThumbsField(ImageField):
    """
    Usage example:
    ==============
    photo = ImageWithThumbsField(upload_to='images', thumbs=((125,125),(300,200),)
        
    Note: The 'thumbs' attribute is not required. If you don't provide it, 
    ImageWithThumbsField will act as a normal ImageField
    """

    attr_class = ImageWithThumbsFieldFile
    default_validators = [IMAGE_EXTENSION_VALIDATOR]

    def __init__(self, *args, **kwargs):
        self.thumbs = kwargs.pop('thumbs', ())
        self.thumbnail_format = kwargs.pop('thumbnail_format', None)

        if 'max_length' not in kwargs:
            kwargs['max_length'] = 255

        super(ImageWithThumbsField, self).__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(ImageWithThumbsField, self).deconstruct()
        # Only include kwarg if it's not the default
        if self.thumbs:
            kwargs['thumbs'] = self.thumbs
        if self.thumbnail_format:
            kwargs['thumbnail_format'] = self.thumbnail_format
        if self.validators == [IMAGE_EXTENSION_VALIDATOR] and 'validators' in kwargs:
            del kwargs['validators']
        if 'storage' in kwargs:
            del kwargs['storage']
        return name, path, args, kwargs