Created
March 24, 2010 15:52
-
-
Save etianen/342425 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Thumbnail generation routines.""" | |
import collections, os | |
from PIL import Image #@UnresolvedImport | |
from django.core.files.storage import default_storage | |
from django.utils import html | |
class Size(collections.namedtuple("Size", "width height")): | |
"""Represents the size of an image.""" | |
__slots__ = () | |
def __new__(cls, width, height): | |
"""Creats a new Size.""" | |
return tuple.__new__(cls, (int(width), int(height))) | |
@property | |
def aspect(self): | |
"""Returns the aspect ratio of this size.""" | |
return float(self.width) / float(self.height) | |
def intersect(self, size): | |
""" | |
Returns a Size that represents the intersection of this and another | |
Size. | |
""" | |
return Size(min(self.width, size.width), min(self.height, size.height)) | |
def constrain(self, reference): | |
""" | |
Returns a new Size that is this Size shrunk to fit inside. | |
""" | |
reference_aspect = reference.aspect | |
width = min(round(self.height * reference_aspect), self.width) | |
height = min(round(self.width / reference_aspect), self.height) | |
return Size(width, height) | |
def scale(self, x_scale, y_scale): | |
"""Returns a new Size with it's width and height scaled.""" | |
return Size(float(self.width) * x_scale, float(self.height) * y_scale) | |
def __str__(self): | |
"""Returns a string representation of this Size.""" | |
return "%ix%i" % (self.width, self.height) | |
# Size adjustment callbacks. These are used to determine the display and data size of the thumbnail. | |
def _size(reference, size): | |
"""Ignores the reference size, and just returns the desired size.""" | |
return size | |
def _size_proportional(reference, size): | |
"""Adjusts the desired size to match the aspect ratio of the reference.""" | |
return size.constrain(reference) | |
# Resize callbacks. These are used to actually resize the image data. | |
def _resize(image, image_size, thumbnail_display_size, thumbnail_image_size): | |
""" | |
Resizes the image to exactly match the desired data size, ignoring aspect | |
ratio. | |
""" | |
return image.resize((thumbnail_image_size), Image.ANTIALIAS) | |
def _resize_cropped(image, image_size, thumbnail_display_size, thumbnail_image_size): | |
""" | |
Resizes the image to fit the desired size, preserving aspect ratio by | |
cropping, if required. | |
""" | |
source_size = image_size.constrain(thumbnail_display_size) | |
source_x = (image_size.width - source_size.width) / 2 | |
source_y = (image_size.height - source_size.height) / 2 | |
return image.transform(thumbnail_image_size, Image.EXTENT, (source_x, source_y, source_x + source_size.width, source_y + source_size.height), Image.BICUBIC) | |
# Methods of generating thumbnails. | |
THUMBNAIL = "thumbnail" | |
RESIZE = "resize" | |
CROP = "crop" | |
_methods = {THUMBNAIL: (_size_proportional, _size, _resize, "resized"), | |
RESIZE: (_size, _size, _resize, "resized"), | |
CROP: (_size, _size_proportional, _resize_cropped, "cropped")} | |
class Thumbnail(object): | |
"""Efficient data structure for holding generated thumbnail data.""" | |
__slots__ = ("name", "path", "url", "display_size", "image_size") | |
def __init__(self, name, path, url, display_size, image_size=None): | |
"""Initializes the Thumbnail.""" | |
self.name = name | |
self.path = path | |
self.url = url | |
self.display_size = display_size | |
self.image_size = image_size or display_size | |
@property | |
def width(self): | |
"""Returns the width of the thumbnail.""" | |
return self.display_size.width | |
@property | |
def height(self): | |
"""Returns the height of the thumbnail.""" | |
return self.display_size.height | |
def __unicode__(self): | |
"""Returns the thumbnail as a XHTML image tag.""" | |
return u'<img src="%s" width="%i" height="%i" alt=""/>' % (html.escape(self.url), self.width, self.height) | |
def __str__(self): | |
"""Returns the thumbnail as a XHTML image tag.""" | |
return str(unicode(self)) | |
def create(image, width, height, method=THUMBNAIL, storage=default_storage): | |
""" | |
Creates a thumbnail from the given image. | |
If the image file is missing or corrupted, then an IOError is raised. | |
""" | |
# Load the image data. | |
image_data = Image.open(image.path) | |
# Get the desired generation method. | |
try: | |
display_size_callback, image_size_callback, resize_callback, folder = _methods[method] | |
except KeyError: | |
raise ValueError, "'%' is not a valid thumbnail generation method. Accepted methods are: %s" % (method, ", ".join(_methods.iterkeys())) | |
# Calculate the final width and height of the thumbnail. | |
image_size = Size._make(image_data.size) | |
thumbnail_display_size = display_size_callback(image_size, Size(width, height)) | |
thumbnail_image_size = image_size_callback(thumbnail_display_size, thumbnail_display_size.intersect(image_size)) | |
# If the file data and thumbnail data are identical, don't bother making a thumbnail. | |
if image_size == thumbnail_image_size: | |
thumbnail_name = image.name | |
thumbnail_path = image.path | |
else: | |
# Use efficient image loading if this would be sensible. | |
if image_size.width < thumbnail_image_size.width and image_size.height < thumbnail_image_size.height: | |
image_data.draft(None, thumbnail_image_size) | |
image_size = Size._make(image_data.size) | |
# Calculate the various file paths. | |
thumbnail_name = "thumbnails/%s/%s/%s" % (folder, thumbnail_image_size, image.name) | |
thumbnail_path = storage.path(thumbnail_name) | |
# Make any intermediate directories. | |
try: | |
os.makedirs(os.path.dirname(thumbnail_path)) | |
except OSError: | |
pass | |
# Check whether the thumbnail exists, and is more recent than the image. | |
image_timestamp = os.stat(image.path).st_mtime | |
try: | |
thumbnail_timestamp = os.stat(thumbnail_path).st_mtime | |
except OSError: | |
# The thumbnail does not exist, so we need to generate it. | |
generation_required = True | |
else: | |
generation_required = image_timestamp > thumbnail_timestamp | |
# If we need to generate the thumbnail, then generate it! | |
if generation_required: | |
# Resize the image data. | |
try: | |
thumbnail_image = resize_callback(image_data, image_size, thumbnail_display_size, thumbnail_image_size) | |
except SyntaxError, ex: | |
# HACK: The PIL will raise a SyntaxError if it encounters a 'broken png'. | |
raise IOError, str(ex) | |
# Save the thumbnail. | |
try: | |
thumbnail_image.save(thumbnail_path) | |
except: | |
# Remove an incomplete file, if present. | |
try: | |
os.unlink(thumbnail_path) | |
except OSError: | |
pass | |
# Re-raise the original exception. | |
raise | |
# Return the thumbnail object. | |
thumbnail_url = storage.url(thumbnail_name) | |
return Thumbnail(thumbnail_name, thumbnail_path, thumbnail_url, thumbnail_display_size, thumbnail_image_size) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment