Ticket #4115: contrib.thumbnails.13.patch
File contrib.thumbnails.13.patch, 23.7 KB (added by , 17 years ago) |
---|
-
django/contrib/thumbnails/base.py
1 import os 2 from StringIO import StringIO 3 from urlparse import urljoin 4 5 from django.conf import settings 6 from django.utils.http import urlquote 7 from PIL import Image 8 9 from exceptions import ThumbnailNoData, ThumbnailInvalidImage 10 from methods import scale 11 12 __all__ = ('Thumbnail',) 13 14 class Thumbnail(object): 15 method = scale 16 base_url = settings.MEDIA_URL 17 root = settings.MEDIA_ROOT 18 19 def __init__(self, filename='', data=None, overwrite=False, size=None, 20 jpeg_quality=None): 21 self._data = data 22 if size: 23 self.size = size 24 self.jpeg_quality = jpeg_quality or 75 25 self.filename = filename 26 self.overwrite = overwrite 27 if data: 28 # If data was given and the thumbnail does not already exist, 29 # generate thumbnail image now. 30 self.make_thumbnail(data) 31 32 def get_filename(self): 33 return self._filename 34 def set_filename(self, filename): 35 if filename: 36 filename = filename % {'x': self.size[0], 'y': self.size[1], 37 'method': self.method.__name__, 38 'jpeg_quality': self.jpeg_quality} 39 filename = os.path.normpath(filename).lstrip(os.sep) 40 if os.path.splitext(filename)[1].lower() not in ('.jpg', '.jpeg'): 41 filename += '.jpg' 42 filename = os.path.join(self.root, filename) 43 self._filename = filename 44 filename = property(get_filename, set_filename) 45 46 def get_thumbnail(self): 47 if hasattr(self, '_thumbnail'): 48 return self._thumbnail 49 try: 50 img = Image.open(self.filename) 51 except IOError, msg: 52 raise ThumbnailInvalidImage(msg) 53 self._thumbnail = img 54 return img 55 thumbnail = property(get_thumbnail) 56 57 def make_thumbnail(self, data): 58 if self.overwrite or not os.path.isfile(self.filename): 59 try: 60 original = Image.open(StringIO(data)) 61 if original.mode not in ("L", "RGB"): 62 original = original.convert("RGB") 63 self._original_image = original 64 thumbnail = self.method() 65 except IOError, msg: 66 raise ThumbnailInvalidImage(msg) 67 self._thumbnail = thumbnail 68 if self.filename: 69 try: 70 thumbnail.save(self.filename, "JPEG", 71 quality=self.jpeg_quality, optimize=1) 72 except IOError: 73 # Try again, without optimization (the JPEG library can't 74 # optimize an image which is larger than ImageFile.MAXBLOCK 75 # which is 64k by default) 76 thumbnail.save(self.filename, "JPEG", 77 quality=self.jpeg_quality) 78 79 def get_url(self): 80 if hasattr(self, '_url'): 81 return self._url 82 filename = self.filename 83 if not os.path.isfile(filename): 84 raise ThumbnailNoData 85 # Get the relative URL by removing the self.root path from the full 86 # thumbnail file name. 87 rel_url = os.path.normpath(filename[len(self.root):]).lstrip(os.sep) 88 if os.sep != '/': 89 rel_url = rel_url.replace(os.sep, '/') 90 # Join the relative URL to the base_url, quoting appropriately. 91 self._url = urljoin(self.base_url, urlquote(rel_url)) 92 return self._url 93 url = property(get_url) 94 95 # Make a straight call to the object from a Django template output it's url. 96 __unicode__ = get_url 97 98 def get_original_image(self): 99 if hasattr(self, '_original_image'): 100 return self._original_image 101 raise ThumbnailNoData 102 original_image = property(get_original_image) 103 104 def delete(self): 105 if os.path.isfile(self.filename): 106 os.remove(self.filename) 107 self.filename = '' -
django/contrib/thumbnails/__init__.py
1 from base import * 2 from exceptions import * 3 from methods import * -
django/contrib/thumbnails/exceptions.py
1 class ThumbnailException(Exception): 2 pass 3 4 class ThumbnailNoData(ThumbnailException): 5 pass 6 7 class ThumbnailTooSmall(ThumbnailException): 8 pass 9 10 class ThumbnailInvalidImage(ThumbnailException): 11 pass -
django/contrib/thumbnails/tests.py
1 import unittest 2 import os 3 4 from django.contrib.thumbnails.templatetags.thumbnails import * 5 from django.conf import settings 6 7 PIC_NAME = "thumbnail-test-pic.jpg" 8 PIC_SIZE = (800, 600) 9 10 class ThumbnailTest(unittest.TestCase): 11 images_to_delete = [] 12 13 def test_thumbnails(self): 14 # Don't run the tests if we can't import Image from PIL 15 try: 16 from PIL import Image 17 except ImportError: 18 return False 19 20 # Create the test image 21 test_image_name = os.path.join(settings.MEDIA_ROOT, PIC_NAME) 22 Image.new('RGB', PIC_SIZE).save(test_image_name, 'JPEG') 23 self.images_to_delete.append(test_image_name) 24 25 # Thumbnail 26 thumb = thumbnail(PIC_NAME, "240x240") 27 thumb_name = "%s_240x240_scale_q75.jpg" % os.path.splitext(PIC_NAME)[0] 28 self.verify_image(thumb, thumb_name, (240, 180)) 29 30 # Squashed thumbnail 31 thumb = thumbnail_squash(PIC_NAME, "240x240") 32 thumb_name = "%s_240x240_squash_q75.jpg" % os.path.splitext(PIC_NAME)[0] 33 self.verify_image(thumb, thumb_name, (240, 240)) 34 35 # Cropped thumbnail 36 thumb = thumbnail_crop(PIC_NAME, "120x240") 37 thumb_name = "%s_120x240_crop_q75.jpg" % os.path.splitext(PIC_NAME)[0] 38 self.verify_image(thumb, thumb_name, (120, 240)) 39 40 # Thumbnail with altered JPEG quality 41 thumb = thumbnail(PIC_NAME, "240x240 85") 42 thumb_name = "%s_240x240_scale_q85.jpg" % os.path.splitext(PIC_NAME)[0] 43 self.verify_image(thumb, thumb_name, (240, 180)) 44 45 def verify_image(self, thumbnail, expected_filename, expected_size): 46 # Verify that the templatetag method returned a thumbnail 47 self.assertTrue(thumbnail) 48 49 # Verify that the file exists 50 full_filename = os.path.join(settings.MEDIA_ROOT, thumbnail.filename) 51 self.assert_(os.path.isfile(thumbnail.filename), 'Thumbnail file not found') 52 53 # Remember to delete the file 54 self.images_to_delete.append(thumbnail.filename) 55 56 # Check that the file name was what we expected 57 expected_filename = os.path.join(settings.MEDIA_ROOT, expected_filename) 58 self.assertEqual(thumbnail.filename, expected_filename) 59 60 # Verify the cropped thumbnail has the expected dimensions 61 im = Image.open(expected_filename) 62 self.assertEqual(im.size, expected_size) 63 64 # Verify the correct url is returned 65 tag = r'<img src="%s" width="%s" height="%s" />' % \ 66 (thumbnail.url, expected_size[0], expected_size[1]) 67 self.assertEqual(tag, img_tag(thumbnail)) 68 69 def tearDown(self): 70 """ 71 Remove all the files that have been created 72 """ 73 for image in self.images_to_delete: 74 os.remove(image) -
django/contrib/thumbnails/templatetags/thumbnails.py
1 import re 2 from os.path import splitext, join, getmtime 3 4 from django.template import Library 5 from django.contrib.thumbnails import Thumbnail, crop, squash, ThumbnailException 6 from django.conf import settings 7 8 register = Library() 9 10 class ThumbnailCrop(Thumbnail): 11 method = crop 12 13 class ThumbnailSquash(Thumbnail): 14 method = squash 15 16 #@register.filter 17 def thumbnail(filename, size): 18 return create_thumbnail(Thumbnail, filename, size) 19 register.filter(thumbnail) 20 21 #@register.filter 22 def thumbnail_crop(filename, size): 23 return create_thumbnail(ThumbnailCrop, filename, size) 24 register.filter(thumbnail_crop) 25 26 #@register.filter 27 def thumbnail_squash(filename, size): 28 return create_thumbnail(ThumbnailSquash, filename, size) 29 register.filter(thumbnail_squash) 30 31 #@register.filter 32 def img_tag(thumbnail): 33 if not thumbnail: 34 return '' 35 x, y = thumbnail.thumbnail.size 36 return '<img src="%s" width="%s" height="%s" />' % (thumbnail.url, x, y) 37 register.filter(img_tag) 38 39 re_size_string = re.compile('\d+') 40 def create_thumbnail(thumbnail_cls, filename, size_string): 41 """ 42 Creates a thumbnail image for the file (which must exist on MEDIA_ROOT) 43 and returns a url to this image. 44 45 If the thumbnail image is not found, an empty string will be returned. 46 """ 47 # Define the size. 48 bits = [int(bit) for bit in re_size_string.findall(size_string)] 49 if len(bits) == 3: 50 size = bits[:2] 51 jpeg_quality = bits[2] 52 elif len(bits) == 2: 53 size = bits 54 jpeg_quality = None 55 else: 56 return '' 57 58 # Define the filename, then create the thumbnail object. 59 basename, ext = splitext(filename) 60 thumbnail_filename = (basename + 61 '_%(x)sx%(y)s_%(method)s_q%(jpeg_quality)s' + ext) 62 original_filename = join(settings.MEDIA_ROOT, filename) 63 64 # See if the thumbnail exists already (and is newer than the original 65 # filename). 66 try: 67 thumbnail = thumbnail_cls(thumbnail_filename, size=size, 68 jpeg_quality=jpeg_quality) 69 if getmtime(original_filename) > getmtime(thumbnail.filename): 70 thumbnail.delete() 71 else: 72 return thumbnail 73 except (ThumbnailException, OSError): 74 # Couldn't get the thumbnail (or something else went wrong). 75 pass 76 77 # Read the original file from disk. 78 try: 79 data = open(original_filename, 'rb').read() 80 except (OSError, IOError): 81 # Couldn't read the original file, or it wasn't a file at all. 82 return '' 83 84 # Generate the thumbnail. 85 try: 86 thumbnail = thumbnail_cls(thumbnail_filename, data, size=size, 87 jpeg_quality=jpeg_quality) 88 except ThumbnailException: 89 return '' 90 91 return thumbnail -
django/contrib/thumbnails/methods.py
1 from PIL import Image, ImageOps 2 3 from exceptions import ThumbnailTooSmall 4 5 def scale(thumbnail): 6 """ Normal PIL thumbnail """ 7 img = thumbnail.original_image 8 size = thumbnail.size 9 if img.size[0] < size[0] and img.size[1] < size[1]: 10 raise ThumbnailTooSmall('Image should be at least %s wide or %s high' % 11 tuple(size)) 12 # .thumbnail() method modifies in-place, so create a copy. 13 img = img.copy() 14 img.thumbnail(size, Image.ANTIALIAS) 15 return img 16 17 def crop(thumbnail): 18 """ Crop the image down to the same ratio as `size` """ 19 img = thumbnail.original_image 20 size = thumbnail.size 21 if img.size[0] < size[0] or img.size[1] < size[1]: 22 raise ThumbnailTooSmall('Image must be at least %s wide and %s high' % 23 tuple(size)) 24 # Just use ImageOps.fit (added in PIL 1.1.3) 25 return ImageOps.fit(img, size, method=Image.ANTIALIAS) 26 27 def squash(thumbnail): 28 """ Resize the image down to exactly `size` (changes ratio) """ 29 img = thumbnail.original_image 30 size = thumbnail.size 31 if img.size[0] < size[0] or img.size[1] < size[1]: 32 raise ThumbnailTooSmall('Image must be at least %s wide and %s high' % 33 tuple(size)) 34 return img.resize(size, Image.ANTIALIAS) -
django/contrib/thumbnails/models.py
1 # Only required so the tests run. 2 No newline at end of file -
docs/thumbnails.txt
1 ========================= 2 django.contrib.thumbnails 3 ========================= 4 5 The ``django.contrib.thumbnails`` package, part of the `"django.contrib" add-ons`_, 6 provides a way of thumbnailing images. 7 8 It requires the Python Imaging Library (PIL_). 9 10 .. _"django.contrib" add-ons: ../add_ons/ 11 .. _PIL: http://www.pythonware.com/products/pil/ 12 13 Template filters 14 ================ 15 16 To use these template filters, add ``'django.contrib.thumbnails'`` to your 17 ``INSTALLED_APPS`` setting. Once you've done that, use 18 ``{% load thumbnails %}`` in a template to give your template access to the 19 filters. 20 21 The thumbnail creation filters, all very similar in behaviour, are: 22 23 * ``thumbnail`` 24 * ``thumbnail_crop`` 25 * ``thumbnail_squash`` 26 27 The only difference between them is the `Thumbnail methods`_ that they use. 28 29 One other filter is provided as a helper to the most common use: 30 31 * ``img_tag`` 32 33 Using the thumbnail filters 34 --------------------------- 35 36 Usage:: 37 38 <img src="{{ object.imagefield|thumbnail:"150x100" }}" /> 39 40 The filter is applied to a image field (not the url get from 41 ``get_[field]_url`` method of the model). Supposing the imagefield filename is 42 ``'image.jpg'``, it creates a thumbnailed image file proportionally resized 43 down to a maximum of 150 pixels wide and 100 pixels high called 44 ``'image_150x100_scale_q75.jpg'`` in the same location as the original image 45 and returns the URL to this thumbnail image. 46 47 The ``thumbnail_crop`` works exactly the same way but uses the crop method 48 (and the filename would be called ``'image_150x100_crop_q75.jpg'``). Similarly, 49 ``thumbnail_squash`` resizes the image to exactly the dimensions given 50 (``'image_150x100_squash_q75.jpg'``). 51 52 If the thumbnail filename already exists, it is only overwritten if the date of 53 the the original image file is newer than the thumbnail file. 54 55 The ``q75`` refers to the JPEG quality of the thumbnail image. You can change 56 the quality by providing a third number to the filter:: 57 58 {{ object.imagefield|thumbnail:"150x10 85" }} 59 60 Rather than just outputting the url, you can reference any other properties 61 (see the ```Thumbnail`` object properties`_ section below for a complete 62 list):: 63 64 {% with object.imagefield|thumbnail:"150x100" as thumb %} 65 <img src="{{ thumb.url }}" width="{{ thumb.thumbnail.size.0 }}" height="{{ thumb.thumbnail.size.1 }}" /> 66 {% endwith %} 67 68 The above example is the most common case, and the ``img_tag`` filter is 69 provided to make that easier. The following example explains it's use:: 70 71 {{ object.imagefield|thumbnail:"150x100"|img_tag }} 72 73 Creating a thumbnail 74 ==================== 75 76 The rest of this documentation deals with lower-level usage of thumbnails. 77 78 To create a thumbnail object, simply call the ``Thumbnail`` class:: 79 80 >>> from django.contrib.thumbnails import * 81 >>> thumbnail = Thumbnail(filename, data, size=(100, 100)) 82 83 The thumbnail object takes the following arguments: 84 85 ================= ========================================================= 86 Argument Description 87 ================= ========================================================= 88 89 ``filename`` A string containing the path and filename to use when 90 saving or retreiving this thumbnail image from disk 91 (relative to the ``Thumbnail`` object's ``root`` property 92 which defaults to ``settings.MEDIA_ROOT``). 93 94 For advanced usage, see the ```Thumbnail```_ property 95 section. 96 97 ``data`` A string or stream of the original image object to be 98 thumbnailed. If not provided and a file matching the 99 thumbnail can not be found, ``TemplateNoData`` will be 100 raised. 101 102 Example:: 103 104 >>> Thumbnail('%(method)/s%(x)sx%(y)s/test.jpg', size=(60, 40)).filename 105 '.../scale/60x40/test.jpg' 106 107 ``overwrite`` Set to ``True`` to overwrite the thumbnail with ``data`` 108 even if an existing cached thumbnail is found. Defaults 109 to ``False``. 110 111 ``size`` The size for the thumbnail image. Required unless using a 112 subclass which provides a default ``size`` (see the 113 `Custom thumbnails`_ section below). 114 115 ``jpeg_quality`` Change the quality of the thumbnail image. The default 116 quality is 75. The PIL manual recommends that values 117 above 95 should be avoided. 118 119 ``Thumbnail`` object properties 120 =============================== 121 122 The thumbnail object which is created provides the following properties and 123 functions: 124 125 ``filename`` 126 ------------ 127 128 Reading this property returns the full path and filename to this thumbnail 129 image. 130 131 When you set this property, the filename string you provide is internally 132 appended to the ``Thumbnail`` object's ``root`` property. 133 134 You can use string formatting to generate the filename based on the 135 thumbnailing method and size: 136 137 * ``%(x)s`` for the thumbnail target width, 138 * ``%(y)s`` for the thumbnail target height, 139 * ``%(method)s`` for the thumbnailing method, 140 * ``%(jpeg_quality)s`` for the JPEG quality. 141 142 For example:: 143 144 >>> Thumbnail('%(method)/s%(x)sx%(y)s/test.jpg', size=(60, 40)).filename 145 '.../scale/60x40/test.jpg' 146 # (where ... is settings.MEDIA_ROOT) 147 148 Note: thumbnailed images are always saved as JPEG images, so if the filename 149 string does not end in `'.jpg'`, this will be automatically appended to the 150 thumbnail's filename. 151 152 ``original_image`` 153 ------------------ 154 155 This read-only property returns a PIL ``Image`` containing the original 156 image (passed in with ``data``). 157 158 ``thumbnail`` 159 ------------- 160 161 This read-only property returns a PIL ``Image`` containing the thumbnail 162 image. 163 164 ``url`` 165 ------- 166 167 This read-only property returns the full url this thumbnail image. 168 169 It is generated by appending the parsed ``filename`` string to the 170 ``Template`` object's ``base_url`` property. 171 172 ``delete()`` 173 ------------ 174 175 Call this function to delete the thumbnail file if it exists on the disk. 176 177 Custom thumbnails 178 ================= 179 180 Similar to newforms, you can create a subclass to override the default 181 properties of the ``Thumbnail`` base class:: 182 183 from django.contrib.thumbnails import Thumbnail 184 185 class MyThumbnail(Thumbnail): 186 size = (100, 100) 187 188 Here are the properties you can provide to your subclass: 189 190 =============== =========================================================== 191 Property Description 192 =============== =========================================================== 193 194 ``size`` Default size for creating thumbnails (no default). 195 ``base_url`` Base url for thumbnails (default is ``settings.MEDIA_URL``). 196 ``root`` Base directory for thumbnails (default is 197 ``settings.MEDIA_ROOT``). 198 ``method`` The thumbnailing funciton to use (default is ``scale``). 199 See the `Thumbnail methods`_ section below. 200 201 Thumbnail methods 202 ================= 203 204 There are several thumbnailing methods available in 205 ``django.contrib.thumbnails.methods`` 206 207 ``crop()`` 208 ---------- 209 210 This method crops the image height or width to match the ratio of the thumbnail 211 ``size`` and then resizes it down to exactly the dimensions of ``size``. 212 213 It requires the original image to be both as wide and as high as ``size``. 214 215 ``scale()`` 216 ----------- 217 218 This is the normal PIL scaling method of proportionally resizing the image down 219 to no greater than the thumbnail ``size`` dimensions. 220 221 It requires the original image to be either as wide or as high as ``size``. 222 223 ``squash()`` 224 ------------ 225 226 This method resizes the image down to exactly the dimensions given. This will 227 potentially squash or stretch the image. 228 229 It requires the original image to be both as wide and as high as ``size``. 230 231 Making your own methods 232 ----------------------- 233 234 To make your own thumbnailing function, create a function which accepts one 235 parameter (``thumbnail``) and returns a PIL ``Image``. 236 237 The ``thumbnail`` parameter will be a ``Thumbnail`` object, so you can use it 238 to get the original image (it will raise ``ThumbnailNoData`` if no data was 239 provided) and the thumbnail size:: 240 241 img = thumbnail.original_image 242 size = thumbnail.size 243 244 Exceptions 245 ========== 246 247 The following exceptions (all found in ``django.contrib.thumbnails.exceptions`` 248 and all subclasses of ``ThumbnailException``) could be raised when using the 249 ``Thumbnail`` object: 250 251 ========================= ================================================ 252 Exception Reason 253 ========================= ================================================ 254 255 ``ThumbnailNoData`` Tried to get the ``original_image`` when no 256 ``data`` was provided or tried to get the 257 ``url`` when the file did not exist and no 258 ``data`` was provided. 259 ``ThumbnailTooSmall`` The ``original_image`` was too small to 260 thumbnail using the given thumbnailing method. 261 ``ThumbnailInvalidImage`` The ``data`` provided could not be decoded to 262 a valid image format (or more rarely, using 263 ``thumbnail`` to retreive an existing thumbnail 264 file from disk which could not be decoded to a 265 valid image format). 266 267 Putting it all together 268 ======================= 269 270 Here is a snippet of an example view which receives an image file from the user 271 and saves a thumbnail of this image to a file named ``[userid].jpg``:: 272 273 from django.contrib.thumbnails import Thumbnail, crop, ThumbnailException 274 275 class ProfileThumbnail(Thumbnail): 276 size = (100, 100) 277 method = crop 278 279 def profile_image(request, id): 280 profile = get_object_or_404(Profile, pk=id) 281 if request.method == 'POST': 282 image = request.FILES.get('profile_image') 283 profile.has_image = False 284 if image: 285 filename = str(profile.id) 286 try: 287 thumbnail = ProfileThumbnail(filename, image['content']) 288 profile.has_image = True 289 except ThumbnailException: 290 pass 291 profile.save() 292 return HttpResponseRedirect('../') 293 ...