Index: django/contrib/thumbnails/base.py
===================================================================
--- django/contrib/thumbnails/base.py	(revision 0)
+++ django/contrib/thumbnails/base.py	(revision 0)
@@ -0,0 +1,94 @@
+from django.conf import settings
+import os
+from StringIO import StringIO
+from PIL import Image
+from exceptions import ThumbnailNoData, ThumbnailInvalidImage
+from methods import scale
+
+__all__ = ('Thumbnail',)
+
+class Thumbnail(object):
+    method = scale
+    base_url = settings.MEDIA_URL
+    root = settings.MEDIA_ROOT
+
+    def __init__(self, filename='', data=None, overwrite=False, size=None, jpeg_quality=None):
+        self._data = data
+        if size:
+            self.size = size
+        self.jpeg_quality = jpeg_quality or 75
+        self.filename = filename
+        self.overwrite = overwrite
+        if data:
+            # If data was given and the thumbnail does not already exist,
+            # generate thumbnail image now.
+            self.make_thumbnail(data)
+
+    def get_filename(self):
+        return self._filename
+    def set_filename(self, filename):
+        if filename:
+            filename = filename % {'x': self.size[0], 'y': self.size[1], 'method': self.method.__name__, 'jpeg_quality': self.jpeg_quality}
+            filename = os.path.normpath(filename).lstrip(os.sep)
+            if os.path.splitext(filename)[1] != '.jpg':
+                filename.append('.jpg')
+            filename = os.path.join(self.root, filename)
+        self._filename = filename
+    filename = property(get_filename, set_filename)
+
+    def get_thumbnail(self):
+        if hasattr(self, '_thumbnail'):
+            return self._thumbnail
+        try:
+            img = Image.open(self.filename)
+        except IOError, msg:
+            raise ThumbnailInvalidImage(msg)
+        self._thumbnail = img
+        return img
+    thumbnail = property(get_thumbnail)
+
+    def make_thumbnail(self, data):
+        if self.overwrite or not os.path.isfile(self.filename):
+            try:
+                original = Image.open(StringIO(data))
+            except IOError, msg:
+                raise ThumbnailInvalidImage(msg)
+            self._original_image = original
+            thumbnail = self.method()
+            self._thumbnail = thumbnail
+            if self.filename:
+                try:
+                    thumbnail.save(self.filename, "JPEG", quality=self.jpeg_quality, optimize=1)
+                except IOError:
+                    # Try again, without optimization (the JPEG library can't
+                    # optimize an image which is larger than ImageFile.MAXBLOCK
+                    # which is 64k by default)
+                    thumbnail.save(self.filename, "JPEG", quality=self.jpeg_quality)
+
+    def get_url(self):
+        if hasattr(self, '_url'):
+            return self._url
+        filename = self.filename
+        if not os.path.isfile(filename):
+            raise ThumbnailNoData
+        url = os.path.normpath(filename[len(self.root):]).lstrip(os.sep)
+        url = os.path.join(self.base_url, url)
+        if os.sep != '/':
+            url = url.replace(os.sep, '/')
+        self._url = url
+        return url
+    url = property(get_url)
+
+    # Make the object will output it's url to Django templates.
+    __str__ = get_url
+
+    def get_original_image(self):
+        if hasattr(self, '_original_image'):
+            return self._original_image
+        raise ThumbnailNoData
+    original_image = property(get_original_image)
+
+    def delete(self):
+        if os.path.isfile(self.filename):
+            os.remove(self.filename)
+        self.filename = ''
Index: django/contrib/thumbnails/__init__.py
===================================================================
--- django/contrib/thumbnails/__init__.py	(revision 0)
+++ django/contrib/thumbnails/__init__.py	(revision 0)
@@ -0,0 +1,3 @@
+from base import *
+from exceptions import *
+from methods import *
Index: django/contrib/thumbnails/exceptions.py
===================================================================
--- django/contrib/thumbnails/exceptions.py	(revision 0)
+++ django/contrib/thumbnails/exceptions.py	(revision 0)
@@ -0,0 +1,11 @@
+class ThumbnailException(Exception):
+    pass
+
+class ThumbnailNoData(ThumbnailException):
+    pass
+
+class ThumbnailTooSmall(ThumbnailException):
+    pass
+
+class ThumbnailInvalidImage(ThumbnailException):
+    pass
Index: django/contrib/thumbnails/methods.py
===================================================================
--- django/contrib/thumbnails/methods.py	(revision 0)
+++ django/contrib/thumbnails/methods.py	(revision 0)
@@ -0,0 +1,53 @@
+from PIL import Image
+from exceptions import ThumbnailTooSmall
+
+
+def scale(thumbnail):
+    """ Normal PIL thumbnail """
+    img = thumbnail.original_image
+    size = thumbnail.size
+    if img.size[0] < size[0] and img.size[1] < size[1]:
+        raise ThumbnailTooSmall('Image should be at least %s wide or %s high' % size)
+    img.thumbnail(size, Image.ANTIALIAS)
+    return img
+
+
+def crop(thumbnail):
+    """ Crop the image down to the same ratio as `size` """
+    img = thumbnail.original_image
+    size = thumbnail.size
+
+    if img.size[0] < size[0] or img.size[1] < size[1]:
+        raise ThumbnailTooSmall('Image must be at least %s wide and %s high' % size)
+
+    image_x, image_y = img.size
+
+    crop_ratio = size[0] / float(size[1])
+    image_ratio = image_x / float(image_y)
+
+    if crop_ratio < image_ratio:
+        # x needs to shrink
+        top = 0
+        bottom = image_y
+        crop_width = int(image_y * crop_ratio)
+        left = (image_x - crop_width) // 2
+        right = left + crop_width
+    else:
+        # y needs to shrink
+        left = 0
+        right = image_x
+        crop_height = int(image_x * crop_ratio)
+        top = (image_y - crop_height) // 2
+        bottom = top + crop_height
+
+    img = img.crop((left, top, right, bottom))
+    return img.resize(size, Image.ANTIALIAS)
+
+
+def squash(thumbnail):
+    """ Resize the image down to exactly `size` (changes ratio) """
+    img = thumbnail.original_image
+    size = thumbnail.size
+    if img.size[0] < size[0] or img.size[1] < size[1]:
+        raise ThumbnailTooSmall('Image must be at least %s wide and %s high' % size)
+    return img.resize(size, Image.ANTIALIAS)
Index: django/contrib/thumbnails/templatetags/__init__.py
===================================================================
Index: django/contrib/thumbnails/templatetags/thumbnails.py
===================================================================
--- django/contrib/thumbnails/templatetags/thumbnails.py	(revision 0)
+++ django/contrib/thumbnails/templatetags/thumbnails.py	(revision 0)
@@ -0,0 +1,94 @@
+from django.template import Library
+from django.contrib.thumbnails import Thumbnail, crop, squash, ThumbnailException
+from django.conf import settings
+from django.utils.html import escape
+import os.path
+import re
+
+register = Library()
+
+class ThumbnailCrop(Thumbnail):
+    method = crop
+
+class ThumbnailSquash(Thumbnail):
+    method = squash
+
+
+#@register.filter
+def thumbnail(file, size):
+    return create_thumbnail(Thumbnail, file, size)
+register.filter(thumbnail)
+
+
+#@register.filter
+def thumbnail_crop(file, size):
+    return create_thumbnail(ThumbnailCrop, file, size)
+register.filter(thumbnail_crop)
+
+
+#@register.filter
+def thumbnail_squash(file, size):
+    return create_thumbnail(ThumbnailSquash, file, size)
+register.filter(thumbnail_squash)
+
+
+#@register.filter
+def img_tag(thumbnail):
+    if not thumbnail:
+        return ''
+    x, y = thumbnail.thumbnail.size
+    url = escape(thumbnail.url)
+    return '<img src="%s" width="%s" height="%s" />' % (url, x, y)
+register.filter(img_tag)
+
+
+re_size_string = re.compile('\d+')
+def create_thumbnail(thumbnail_cls, file, size_string):
+    """
+    Creates a thumbnail image for the file (which must exist on MEDIA_ROOT)
+    and returns a url to this image.
+    
+    If the thumbnail image is not found, an empty string will be returned.
+    """
+    # Define the size.
+    bits = [int(bit) for bit in re_size_string.findall(size_string)]
+    if len(bits) == 3:
+        size = bits[:2]
+        jpeg_quality = bits[2]
+    elif len(bits) == 2:
+        size = bits
+        jpeg_quality = None
+    else:
+        return ''
+    
+    # Define the filename, then create the thumbnail object.
+    basename, ext = os.path.splitext(file)
+    thumbnail_filename = basename + '_%(x)sx%(y)s_%(method)s_q%(jpeg_quality)s' + ext
+    original_filename = os.path.join(settings.MEDIA_ROOT, file)
+    
+    # See if the thumbnail exists already (and is newer than the
+    # original filename).
+    try:
+        thumbnail = thumbnail_cls(thumbnail_filename, size=size, jpeg_quality=jpeg_quality)
+        if os.path.getmtime(original_filename) > os.path.getmtime(thumbnail.filename):
+            thumbnail.delete()
+        else:
+            return thumbnail
+    except (ThumbnailException, OSError):
+        # Couldn't get the thumbnail (or something else went wrong).
+        pass
+
+    # Read the original file from disk.
+    try:
+        data = open(original_filename, 'rb').read()
+    except OSError:
+        # Couldn't read the original file.
+        return ''
+    
+    # Generate the thumbnail.
+    try:
+        thumbnail = thumbnail_cls(thumbnail_filename, data, size=size, jpeg_quality=jpeg_quality)
+    except ThumbnailException:
+        return ''
+
+    return thumbnail
Index: docs/thumbnails.txt
===================================================================
--- docs/thumbnails.txt	(revision 0)
+++ docs/thumbnails.txt	(revision 0)
@@ -0,0 +1,293 @@
+=========================
+django.contrib.thumbnails
+=========================
+
+The ``django.contrib.thumbnails`` package, part of the `"django.contrib" add-ons`_,
+provides a way of thumbnailing images.
+
+It requires the Python Imaging Library (PIL_).
+
+.. _"django.contrib" add-ons: ../add_ons/
+.. _PIL: http://www.pythonware.com/products/pil/
+
+Template filters
+================
+
+To use these template filters, add ``'django.contrib.thumbnails'`` to your
+``INSTALLED_APPS`` setting. Once you've done that, use
+``{% load thumbnails %}`` in a template to give your template access to the
+filters.
+
+The thumbnail creation filters, all very similar in behaviour, are:
+
+ * ``thumbnail``
+ * ``thumbnail_crop``
+ * ``thumbnail_squash``
+
+The only difference between them is the `Thumbnail methods`_ that they use.
+
+One other filter is provided as a helper to the most common use:
+
+ * ``img_tag``
+
+Using the thumbnail filters
+---------------------------
+
+Usage::
+
+    <img src="{{ object.imagefield|thumbnail:"150x100" }}" />
+
+The filter is applied to a image field (not the url get from 
+``get_[field]_url`` method of the model). Supposing the imagefield filename is
+``'image.jpg'``, it creates a thumbnailed image file proportionally resized
+down to a maximum of 150 pixels wide and 100 pixels high called
+``'image_150x100_scale_q75.jpg'`` in the same location as the original image
+and returns the URL to this thumbnail image.
+
+The ``thumbnail_crop`` works exactly the same way but uses the crop method
+(and the filename would be called ``'image_150x100_crop_q75.jpg'``). Similarly,
+``thumbnail_squash`` resizes the image to exactly the dimensions given
+(``'image_150x100_squash_q75.jpg'``).
+
+If the thumbnail filename already exists, it is only overwritten if the date of
+the the original image file is newer than the thumbnail file.
+
+The ``q75`` refers to the JPEG quality of the thumbnail image. You can change
+the quality by providing a third number to the filter::
+
+    {{ object.imagefield|thumbnail:"150x10 85" }}
+
+Rather than just outputting the url, you can reference any other properties
+(see the ```Thumbnail`` object properties`_ section below for a complete
+list)::
+
+    {% with object.imagefield|thumbnail:"150x100" as thumb %}
+    <img src="{{ thumb.url }}" width="{{ thumb.thumbnail.size.0 }}" height="{{ thumb.thumbnail.size.1 }}" />
+    {% endwith %}
+
+The above example is the most common case, and the ``img_tag`` filter is
+provided to make that easier. The following example explains it's use::
+
+	{{ object.imagefield|thumbnail:"150x100"|img_tag }}
+
+Creating a thumbnail
+====================
+
+The rest of this documentation deals with lower-level usage of thumbnails.
+
+To create a thumbnail object, simply call the ``Thumbnail`` class::
+
+    >>> from django.contrib.thumbnails import *
+    >>> thumbnail = Thumbnail(filename, data, size=(100, 100))
+
+The thumbnail object takes the following arguments:
+
+    ================= =========================================================
+     Argument          Description
+    ================= =========================================================
+
+    ``filename``      A string containing the path and filename to use when
+                      saving or retreiving this thumbnail image from disk
+                      (relative to the ``Thumbnail`` object's ``root`` property
+                      which defaults to ``settings.MEDIA_ROOT``).
+
+                      For advanced usage, see the ```Thumbnail```_ property
+                      section.
+
+    ``data``          A string or stream of the original image object to be
+                      thumbnailed. If not provided and a file matching the
+                      thumbnail can not be found, ``TemplateNoData`` will be
+                      raised.
+
+                      Example::
+
+                          >>> Thumbnail('%(method)/s%(x)sx%(y)s/test.jpg', size=(60, 40)).filename
+                          '.../scale/60x40/test.jpg'
+
+    ``overwrite``     Set to ``True`` to overwrite the thumbnail with ``data``
+                      even if an existing cached thumbnail is found. Defaults
+                      to ``False``.
+
+    ``size``          The size for the thumbnail image. Required unless using a
+                      subclass which provides a default ``size`` (see the
+                      `Custom thumbnails`_ section below).
+
+    ``jpeg_quality``  Change the quality of the thumbnail image. The default
+                      quality is 75. The PIL manual recommends that values
+                      above 95 should be avoided.
+
+``Thumbnail`` object properties
+===============================
+
+The thumbnail object which is created provides the following properties and
+functions:
+
+``filename``
+------------
+
+Reading this property returns the full path and filename to this thumbnail
+image.
+
+When you set this property, the filename string you provide is internally
+appended to the ``Thumbnail`` object's ``root`` property.
+
+You can use string formatting to generate the filename based on the
+thumbnailing method and size:
+
+  * ``%(x)s`` for the thumbnail target width,
+  * ``%(y)s`` for the thumbnail target height,
+  * ``%(method)s`` for the thumbnailing method,
+  * ``%(jpeg_quality)s`` for the JPEG quality.
+
+For example::
+
+    >>> Thumbnail('%(method)/s%(x)sx%(y)s/test.jpg', size=(60, 40)).filename
+    '.../scale/60x40/test.jpg'
+    # (where ... is settings.MEDIA_ROOT)
+
+Note: thumbnailed images are always saved as JPEG images, so if the filename
+string does not end in `'.jpg'`, this will be automatically appended to the
+thumbnail's filename.
+
+``original_image``
+------------------
+
+This read-only property returns a PIL ``Image`` containing the original
+image (passed in with ``data``).
+
+``thumbnail``
+-------------
+
+This read-only property returns a PIL ``Image`` containing the thumbnail
+image.
+
+``url``
+-------
+
+This read-only property returns the full url this thumbnail image.
+
+It is generated by appending the parsed ``filename`` string to the 
+``Template`` object's ``base_url`` property.
+
+``delete()``
+------------
+
+Call this function to delete the thumbnail file if it exists on the disk.
+
+Custom thumbnails
+=================
+
+Similar to newforms, you can create a subclass to override the default
+properties of the ``Thumbnail`` base class::
+
+    from django.contrib.thumbnails import Thumbnail
+
+    class MyThumbnail(Thumbnail):
+        size = (100, 100)
+
+Here are the properties you can provide to your subclass:
+
+    =============== ===========================================================
+     Property        Description
+    =============== ===========================================================
+
+    ``size``        Default size for creating thumbnails (no default).
+    ``base_url``    Base url for thumbnails (default is ``settings.MEDIA_URL``).
+    ``root``        Base directory for thumbnails (default is
+                    ``settings.MEDIA_ROOT``).
+    ``method``      The thumbnailing funciton to use (default is ``scale``).
+                    See the `Thumbnail methods`_ section below.
+
+Thumbnail methods
+=================
+
+There are several thumbnailing methods available in
+``django.contrib.thumbnails.methods``
+
+``crop()``
+----------
+
+This method crops the image height or width to match the ratio of the thumbnail
+``size`` and then resizes it down to exactly the dimensions of ``size``.
+
+It requires the original image to be both as wide and as high as ``size``.
+
+``scale()``
+-----------
+
+This is the normal PIL scaling method of proportionally resizing the image down
+to no greater than the thumbnail ``size`` dimensions.
+
+It requires the original image to be either as wide or as high as ``size``.
+
+``squash()``
+------------
+
+This method resizes the image down to exactly the dimensions given. This will
+potentially squash or stretch the image.
+
+It requires the original image to be both as wide and as high as ``size``.
+
+Making your own methods
+-----------------------
+
+To make your own thumbnailing function, create a function which accepts one
+parameter (``thumbnail``) and returns a PIL ``Image``.
+
+The ``thumbnail`` parameter will be a ``Thumbnail`` object, so you can use it
+to get the original image (it will raise ``ThumbnailNoData`` if no data was
+provided) and the thumbnail size::
+
+    img = thumbnail.original_image
+    size = thumbnail.size
+
+Exceptions
+==========
+
+The following exceptions (all found in ``django.contrib.thumbnails.exceptions``
+and all subclasses of ``ThumbnailException``) could be raised when using the
+``Thumbnail`` object:
+
+    =========================  ================================================
+     Exception                  Reason
+    =========================  ================================================
+
+    ``ThumbnailNoData``        Tried to get the ``original_image`` when no
+                               ``data`` was provided or tried to get the
+                               ``url`` when the file did not exist and no
+                               ``data`` was provided.
+    ``ThumbnailTooSmall``      The ``original_image`` was too small to
+                               thumbnail using the given thumbnailing method.
+    ``ThumbnailInvalidImage``  The ``data`` provided could not be decoded to
+                               a valid image format (or more rarely, using
+                               ``thumbnail`` to retreive an existing thumbnail
+                               file from disk which could not be decoded to a
+                               valid image format).
+
+Putting it all together
+=======================
+
+Here is a snippet of an example view which receives an image file from the user
+and saves a thumbnail of this image to a file named ``[userid].jpg``::
+
+    from django.contrib.thumbnails import Thumbnail, crop, ThumbnailException
+
+    class ProfileThumbnail(Thumbnail):
+        size = (100, 100)
+        method = crop
+
+    def profile_image(request, id):
+        profile = get_object_or_404(Profile, pk=id)
+        if request.method == 'POST':
+            image = request.FILES.get('profile_image')
+            profile.has_image = False
+            if image:
+                filename = str(profile.id)
+                try:
+                    thumbnail = ProfileThumbnail(filename, image['content'])
+                    profile.has_image = True
+                except ThumbnailException:
+                    pass
+            profile.save()
+            return HttpResponseRedirect('../')
+        ...
