Index: django/contrib/thumbnails/base.py
===================================================================
--- django/contrib/thumbnails/base.py	(revision 0)
+++ django/contrib/thumbnails/base.py	(revision 0)
@@ -0,0 +1,97 @@
+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].lower() not in ('.jpg', '.jpeg'):
+                filename += '.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_src = Image.open(StringIO(data))
+                original = original_src.copy()
+                if original.mode not in ("L", "RGB"):
+                    original = original.convert("RGB")
+                self._original_image = original
+                thumbnail = self.method()
+                self._thumbnail = thumbnail
+            except IOError, msg:
+                raise ThumbnailInvalidImage(msg)
+            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 = ''

Property changes on: django/contrib/thumbnails/base.py
___________________________________________________________________
Name: svn:executable
   + *

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,31 @@
+from PIL import Image, ImageOps
+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' % tuple(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' % tuple(size))
+    # Just use ImageOps.fit (added in PIL 1.1.3)
+    return ImageOps.fit(img, size, method=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' % tuple(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,97 @@
+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 ''
+    except IOError:
+        # Looks like it wasn't a file at all
+        return ''
+    
+    # Generate the thumbnail.
+    try:
+        thumbnail = thumbnail_cls(thumbnail_filename, data, size=size, jpeg_quality=jpeg_quality)
+    except ThumbnailException:
+        return ''
+
+    return thumbnail

Property changes on: django/contrib/thumbnails/templatetags/thumbnails.py
___________________________________________________________________
Name: svn:executable
   + *

Index: tests/regressiontests/thumbnails/__init__.py
===================================================================
Index: tests/regressiontests/thumbnails/sample-pic.jpg
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: tests/regressiontests/thumbnails/sample-pic.jpg
___________________________________________________________________
Name: svn:executable
   + *
Name: svn:mime-type
   + application/octet-stream

Index: tests/regressiontests/thumbnails/tests.py
===================================================================
--- tests/regressiontests/thumbnails/tests.py	(revision 0)
+++ tests/regressiontests/thumbnails/tests.py	(revision 0)
@@ -0,0 +1,77 @@
+"""
+Test the functionality of the thumbnail contribution
+"""
+import unittest
+from django.template.defaultfilters import *
+from regressiontests.thumbnails.models import Picture
+from django.contrib.thumbnails.templatetags.thumbnails import * 
+import os
+from PIL import Image
+
+class ThumbnailTest(unittest.TestCase):
+    pic_name = "sample-pic.jpg"
+    
+    def testThumbnail(self):
+        # Create a dummy picture
+        p1 = Picture()
+        p1.image = self.pic_name
+        p1.save()
+        
+        # Create a thumbnail and verify the file exists where it should
+        t1 = thumbnail(p1.image, "240x240")
+        thumb_name = "%s_240x240_scale_q75.jpg" % os.path.splitext(self.pic_name)[0]
+        self.thumb = os.path.join(settings.MEDIA_ROOT, thumb_name)
+        self.assertEqual(os.path.isfile(self.thumb), True)
+        
+        # Verify the thumbnail has the expected dimensions
+        im = Image.open(self.thumb)
+        self.assertEqual(im.size, (240,192))
+        
+        # Verify the correct url is returned
+        url = r'<img src="%s" width="240" height="192" />' % t1.url
+        self.assertEqual(url, img_tag(t1))
+        
+        
+        # Create a squashed image and verify the file exists where it should
+        t2 = thumbnail_squash(p1.image, "240x240")
+        squash_name = "%s_240x240_squash_q75.jpg" % os.path.splitext(self.pic_name)[0]
+        self.squash = os.path.join(settings.MEDIA_ROOT, squash_name)
+        self.assertEqual(os.path.isfile(self.squash), True)
+        
+        # Verify the squashed thumbnail has the expected dimensions
+        im = Image.open(self.squash)
+        self.assertEqual(im.size, (240,240))
+        
+        # Verify the correct url is returned
+        url = r'<img src="%s" width="240" height="240" />' % t2.url
+        self.assertEqual(url, img_tag(t2))
+        
+        # Create a cropped image and verify the file exists where it should
+        t3 = thumbnail_crop(p1.image, "120x240")
+        crop_name = "%s_120x240_crop_q75.jpg" % os.path.splitext(self.pic_name)[0]
+        self.crop = os.path.join(settings.MEDIA_ROOT, crop_name)
+        self.assertEqual(os.path.isfile(self.crop), True)
+        
+        # Verify the cropped thumbnail has the expected dimensions
+        im = Image.open(self.crop)
+        self.assertEqual(im.size, (120,240))
+        
+        # Verify the correct url is returned
+        url = r'<img src="%s" width="120" height="240" />' % t3.url
+        self.assertEqual(url, img_tag(t3))
+    
+    
+    def tearDown(self):
+        """
+        Remove all the files that have been created
+        """
+        dest_image = os.path.join(settings.MEDIA_ROOT, self.pic_name)
+        os.remove(dest_image)
+        os.remove(self.thumb)
+        os.remove(self.squash)
+        os.remove(self.crop)
+        
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
\ No newline at end of file

Property changes on: tests/regressiontests/thumbnails/tests.py
___________________________________________________________________
Name: svn:executable
   + *

Index: tests/regressiontests/thumbnails/models.py
===================================================================
--- tests/regressiontests/thumbnails/models.py	(revision 0)
+++ tests/regressiontests/thumbnails/models.py	(revision 0)
@@ -0,0 +1,20 @@
+from django.db import models
+import tempfile
+from django.conf import settings
+import os
+import shutil
+
+class Picture(models.Model):
+    image = models.ImageField(upload_to="/")
+
+    def save(self):
+        """
+        During the save operation, we need to copy the file to the
+        media_root location.  This simulates what would happen with a file
+        upload.
+        We'll handle deleting in the unit test
+        """
+        src_image = os.path.join(os.path.dirname(__file__), "sample-pic.jpg")
+        dest_image = os.path.join(settings.MEDIA_ROOT, "sample-pic.jpg")
+        shutil.copyfile(src_image, dest_image)
+        super(Picture, self).save()
\ No newline at end of file

Property changes on: tests/regressiontests/thumbnails/models.py
___________________________________________________________________
Name: svn:executable
   + *

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('../')
+        ...
