Index: django/core/meta/__init__.py
===================================================================
--- django/core/meta/__init__.py	(revision 1508)
+++ django/core/meta/__init__.py	(working copy)
@@ -839,6 +839,8 @@
                         setattr(new_class, 'get_%s_width' % f.name, curry(method_get_image_width, f))
                     if not f.height_field:
                         setattr(new_class, 'get_%s_height' % f.name, curry(method_get_image_height, f))
+                    # thumbnail support
+                    setattr(new_class, 'get_%s_thumbnail' % f.name, curry(method_get_image_thumbnail, f))
 
         # Add the class itself to the new module we've created.
         new_mod.__dict__[name] = new_class
@@ -1066,6 +1068,14 @@
             # delete it from the filesystem.
             if os.path.exists(file_name) and not opts.get_model_module().get_list(**{'%s__exact' % f.name: getattr(self, f.name)}):
                 os.remove(file_name)
+                if isinstance(f, ImageField):
+                    # remove thumbnails
+                    import fnmatch
+                    base, ext = os.path.splitext(os.path.basename(file_name))
+                    basedir = os.path.dirname(file_name)
+                    for file in fnmatch.filter(os.listdir(basedir), '%s_t*%s' % (base, ext)):
+                        os.remove(os.path.join(basedir, file))
+
     # Run any post-delete hooks.
     if hasattr(self, '_post_delete'):
         self._post_delete()
@@ -1322,6 +1332,19 @@
         setattr(self, cachename, get_image_dimensions(fname))
     return getattr(self, cachename)
 
+def method_get_image_thumbnail(field, self, width=None, height=None, root=settings.MEDIA_ROOT, url_root=settings.MEDIA_URL):
+    from django.parts.media.photos import make_thumbnail
+    url = getattr(self, "get_%s_url" % field.name)()
+    
+    if (width is None) and (height is None):
+        return url
+
+    th = make_thumbnail(url, width, height, root, url_root)
+    if th is not None:
+        return th
+    else:
+        return url
+
 ##############################################
 # HELPER FUNCTIONS (CURRIED MODEL FUNCTIONS) #
 ##############################################
Index: django/parts/media/photos.py
===================================================================
--- django/parts/media/photos.py	(revision 1508)
+++ django/parts/media/photos.py	(working copy)
@@ -1,6 +1,181 @@
-import re
+import re, os
+from django.conf.settings import MEDIA_ROOT, MEDIA_URL
+import urlparse
+# check for PIL
+try:
+    import Image
+    HAS_PIL = True
+except ImportError:
+    HAS_PIL = False
 
-def get_thumbnail_url(photo_url, width):
-    bits = photo_url.split('/')
-    bits[-1] = re.sub(r'(?i)\.(gif|jpg)$', '_t%s.\\1' % width, bits[-1])
-    return '/'.join(bits)
+def _get_thumbnail_path(path, width=None, height=None):
+    """ create thumbnail path from path and required width and/or height.
+    
+        thumbnail file name is constructed like this:
+            <basename>_t_[w<width>][_h<height>].<extension>
+    """
+    
+    # one of width/height is required
+    assert (width is not None) or (height is not None)
+
+    basedir = os.path.dirname(path) + '/'
+    base, ext = os.path.splitext(os.path.basename(path))
+    
+    # make thumbnail filename
+    th_name = base + '_t'
+    if (width is not None) and (height is not None):
+        th_name += '_w%d_h%d' % (width, height)
+    elif width is not None:
+        th_name += '_w%d' % width
+    elif height is not None:
+        th_name += '_h%d' % height
+    th_name += ext
+    
+    return urlparse.urljoin(basedir, th_name)
+#
+
+def get_path_from_url(url, root=MEDIA_ROOT, url_root=MEDIA_URL):
+    """ make filesystem path from url """
+
+    if url.startswith(url_root):
+        url = url[len(url_root):] # strip media root url
+
+    return os.path.normpath(os.path.join(root, url))
+#
+
+def get_url_from_path(path, root=MEDIA_ROOT, url_root=MEDIA_URL):
+    """ make url from filesystem path """
+
+    if path.startswith(root):
+        path = path[len(root):] # strip media root
+    
+    return urlparse.urljoin(root, path.replace('\\', '/'))
+#
+
+def has_thumbnail(photo_url, width=None, height=None, root=MEDIA_ROOT, url_root=MEDIA_URL):
+    # one of width/height is required
+    assert (width is not None) or (height is not None)
+
+    return os.path.isfile(get_path_from_url(_get_thumbnail_path(photo_url, width, height), root, url_root))
+#
+
+def make_thumbnail(photo_url, width=None, height=None, root=MEDIA_ROOT, url_root=MEDIA_URL):
+    """ create thumbnail """
+    
+    # one of width/height is required
+    assert (width is not None) or (height is not None)
+    
+    if not HAS_PIL: return None # no PIL - no thumbnail
+
+    th_url = _get_thumbnail_path(photo_url, width, height)
+    th_path = get_path_from_url(th_url, root, url_root)
+    photo_path = get_path_from_url(photo_url, root, url_root)
+    
+    if has_thumbnail(photo_url, width, height, root, url_root):
+        # thumbnail already exists
+        if not (os.path.getmtime(photo_path) > os.path.getmtime(th_path)):
+            # if photo mtime is newer than thumbnail recreate thumbnail
+            return th_url
+    
+    # make thumbnail
+    
+    # get original image size
+    orig_w, orig_h = get_image_size(photo_url, root, url_root)
+    
+    if (orig_w == width) and (orig_h == height):
+        # same dimensions
+        return None
+
+    img = Image.open(photo_path).copy()
+    # make proper size
+    if (width is not None) and (height is not None):
+        size = (width, height)
+    elif width is not None:
+        size = (width, orig_h)
+    elif height is not None:
+        size = (orig_w, height)
+
+    img.thumbnail(size, Image.ANTIALIAS)
+    img.save(th_path)
+
+    return th_url
+#
+
+def get_thumbnail_url(photo_url, width=None, height=None, root=MEDIA_ROOT, url_root=MEDIA_URL):
+    """ return thumbnail URL for requested photo_url and required width and/or height
+    
+        if thumbnail file do not exists returns original URL
+    """
+
+    # one of width/height is required
+    assert (width is not None) or (height is not None)
+    
+    if has_thumbnail(photo_url, width, height, root, url_root):
+        return _get_thumbnail_path(photo_url, width, height)
+    else:
+        return photo_url
+
+def _no_pil_image_size(fname):
+    """
+        Determine the image type of FNAME and return its size.
+        ripped from draco
+        
+        returns tuple (width, height) or None
+    """
+    
+    try:
+        filehandle = file(fname, 'rb')
+    except IOError:
+        return None
+
+    head = filehandle.read(24)
+    if len(head) != 24:
+        return
+    if head[:4] == '\x89PNG':
+        # PNG
+        check = struct.unpack('>i', head[4:8])[0]
+        if check != 0x0d0a1a0a:
+            return
+        width, height = struct.unpack('>ii', head[16:24])
+    elif head[:6] in ('GIF87a', 'GIF89a'):
+        # GIF
+        width, height = struct.unpack('<HH', head[6:10])
+    elif head[:4] == '\xff\xd8\xff\xe0' and head[6:10] == 'JFIF':
+        # JPEG
+        try:
+            filehandle.seek(0)  # Read 0xff next
+            size = 2
+            filetype = 0
+            while not 0xc0 <= filetype <= 0xcf:
+                filehandle.seek(size, 1)
+                byte = filehandle.read(1)
+                while ord(byte) == 0xff:
+                    byte = filehandle.read(1)
+                filetype = ord(byte)
+                size = struct.unpack('>H', filehandle.read(2))[0] - 2
+            # We are at a SOFn block
+            filehandle.seek(1, 1)  # Skip `precision' byte.
+            height, width = struct.unpack('>HH', filehandle.read(4))
+        except:
+            raise
+            return
+    else:
+        return
+    return width, height
+#
+
+def get_image_size(photo_url, root=MEDIA_ROOT, url_root=MEDIA_URL):
+    """ returns image size, use PIL if present or _no_pil_image_size if no PIL is found.
+    
+        TODO: add image size caching.
+    """
+    
+    path = get_path_from_url(photo_url, root, url_root)
+    
+    if HAS_PIL:
+        size = Image.open(path).size
+    else:
+        size = _no_pil_image_size(path)
+
+    return size
+#
Index: django/contrib/admin/templatetags/admin_list.py
===================================================================
--- django/contrib/admin/templatetags/admin_list.py	(revision 1508)
+++ django/contrib/admin/templatetags/admin_list.py	(working copy)
@@ -151,7 +151,7 @@
             # ImageFields are special: Use a thumbnail.
             elif isinstance(f, meta.ImageField):
                 from django.parts.media.photos import get_thumbnail_url
-                result_repr = '<img src="%s" alt="%s" title="%s" />' % (get_thumbnail_url(getattr(result, 'get_%s_url' % f.name)(), '120'), field_val, field_val)
+                result_repr = '<img src="%s" alt="%s" title="%s" />' % (getattr(result, 'get_%s_thumbnail' % f.name)(width=120), field_val, field_val)
             # FloatFields are special: Zero-pad the decimals.
             elif isinstance(f, meta.FloatField):
                 if field_val is not None:
