Ticket #4115: contrib.thumbnails.4.patch
File contrib.thumbnails.4.patch, 23.5 KB (added by , 18 years ago) |
---|
-
django/contrib/thumbnails/base.py
1 from django.conf import settings 2 import os 3 from StringIO import StringIO 4 from PIL import Image 5 from exceptions import ThumbnailNoData, ThumbnailInvalidImage 6 from methods import scale 7 8 __all__ = ('Thumbnail',) 9 10 class Thumbnail(object): 11 method = scale 12 base_url = settings.MEDIA_URL 13 root = settings.MEDIA_ROOT 14 15 def __init__(self, filename='', data=None, overwrite=False, size=None, jpeg_quality=None): 16 self._data = data 17 if size: 18 self.size = size 19 self.jpeg_quality = jpeg_quality or 75 20 self.filename = filename 21 self.overwrite = overwrite 22 if data: 23 # If data was given and the thumbnail does not already exist, 24 # generate thumbnail image now. 25 self.make_thumbnail(data) 26 27 def get_filename(self): 28 return self._filename 29 def set_filename(self, filename): 30 if filename: 31 filename = filename % {'x': self.size[0], 'y': self.size[1], 'method': self.method.__name__, 'jpeg_quality': self.jpeg_quality} 32 filename = os.path.normpath(filename).lstrip(os.sep) 33 if os.path.splitext(filename)[1] != '.jpg': 34 filename.append('.jpg') 35 filename = os.path.join(self.root, filename) 36 self._filename = filename 37 filename = property(get_filename, set_filename) 38 39 def get_thumbnail(self): 40 if hasattr(self, '_thumbnail'): 41 return self._thumbnail 42 try: 43 img = Image.open(self.filename) 44 except IOError, msg: 45 raise ThumbnailInvalidImage(msg) 46 self._thumbnail = img 47 return img 48 thumbnail = property(get_thumbnail) 49 50 def make_thumbnail(self, data): 51 if self.overwrite or not os.path.isfile(self.filename): 52 try: 53 original = Image.open(StringIO(data)) 54 except IOError, msg: 55 raise ThumbnailInvalidImage(msg) 56 self._original_image = original 57 thumbnail = self.method() 58 self._thumbnail = thumbnail 59 if self.filename: 60 try: 61 thumbnail.save(self.filename, "JPEG", quality=self.jpeg_quality, optimize=1) 62 except IOError: 63 # Try again, without optimization (the JPEG library can't 64 # optimize an image which is larger than ImageFile.MAXBLOCK 65 # which is 64k by default) 66 thumbnail.save(self.filename, "JPEG", quality=self.jpeg_quality) 67 68 def get_url(self): 69 if hasattr(self, '_url'): 70 return self._url 71 filename = self.filename 72 if not os.path.isfile(filename): 73 raise ThumbnailNoData 74 url = os.path.normpath(filename[len(self.root):]).lstrip(os.sep) 75 url = os.path.join(self.base_url, url) 76 if os.sep != '/': 77 url = url.replace(os.sep, '/') 78 self._url = url 79 return url 80 url = property(get_url) 81 82 # Make the object will output it's url to Django templates. 83 __str__ = get_url 84 85 def get_original_image(self): 86 if hasattr(self, '_original_image'): 87 return self._original_image 88 raise ThumbnailNoData 89 original_image = property(get_original_image) 90 91 def delete(self): 92 if os.path.isfile(self.filename): 93 os.remove(self.filename) 94 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 from django.contrib.thumbnails.templatetags.thumbnails import * 2 from django.conf import settings 3 4 import unittest 5 import os 6 7 try: 8 from PIL import Image 9 except ImportError: 10 Image = None 11 12 PIC_NAME = "thumbmnail-test-pic.jpg" 13 PIC_SIZE = (800, 600) 14 15 class ThumbnailTest(unittest.TestCase): 16 images_to_delete = [] 17 18 def testThumbnail(self): 19 # Don't run the tests if we couldn't import Image from PIL 20 if not Image: 21 return False 22 23 # Create the test image 24 test_image_name = os.path.join(settings.MEDIA_ROOT, PIC_NAME) 25 Image.new('RGB', PIC_SIZE).save(test_image_name, 'JPEG') 26 self.images_to_delete.append(test_image_name) 27 28 # Thumbnail 29 thumb = thumbnail(PIC_NAME, "240x240") 30 thumb_name = "%s_240x240_scale_q75.jpg" % os.path.splitext(PIC_NAME)[0] 31 self.verify_image(thumb, thumb_name, (240, 180)) 32 33 # Squashed thumbanil 34 thumb = thumbnail_squash(PIC_NAME, "240x240") 35 thumb_name = "%s_240x240_squash_q75.jpg" % os.path.splitext(PIC_NAME)[0] 36 self.verify_image(thumb, thumb_name, (240, 240)) 37 38 # Cropped thumbnail 39 thumb = thumbnail_crop(PIC_NAME, "120x240") 40 thumb_name = "%s_120x240_crop_q75.jpg" % os.path.splitext(PIC_NAME)[0] 41 self.verify_image(thumb, thumb_name, (120, 240)) 42 43 # Thumbnail with altered JPEG quality 44 thumb = thumbnail(PIC_NAME, "240x240 85") 45 thumb_name = "%s_240x240_scale_q85.jpg" % os.path.splitext(PIC_NAME)[0] 46 self.verify_image(thumb, thumb_name, (240, 180)) 47 48 def verify_image(self, thumbnail, expected_filename, expected_size): 49 # Verify the file exists 50 expected_filename = os.path.join(settings.MEDIA_ROOT, expected_filename) 51 self.assertEqual(os.path.isfile(expected_filename), True) 52 53 # Remember to delete this image 54 self.images_to_delete.append(expected_filename) 55 56 # Verify the cropped thumbnail has the expected dimensions 57 im = Image.open(expected_filename) 58 self.assertEqual(im.size, expected_size) 59 60 # Verify the correct url is returned 61 tag = r'<img src="%s" width="%s" height="%s" />' % \ 62 (thumbnail.url, expected_size[0], expected_size[1]) 63 self.assertEqual(tag, img_tag(thumbnail)) 64 65 def tearDown(self): 66 """ 67 Remove all the files that have been created 68 """ 69 for image in self.images_to_delete: 70 os.remove(image) -
django/contrib/thumbnails/methods.py
1 from PIL import Image 2 from exceptions import ThumbnailTooSmall 3 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' % size) 11 img.thumbnail(size, Image.ANTIALIAS) 12 return img 13 14 15 def crop(thumbnail): 16 """ Crop the image down to the same ratio as `size` """ 17 img = thumbnail.original_image 18 size = thumbnail.size 19 20 if img.size[0] < size[0] or img.size[1] < size[1]: 21 raise ThumbnailTooSmall('Image must be at least %s wide and %s high' % size) 22 23 image_x, image_y = img.size 24 25 crop_ratio = size[0] / float(size[1]) 26 image_ratio = image_x / float(image_y) 27 28 if crop_ratio < image_ratio: 29 # x needs to shrink 30 top = 0 31 bottom = image_y 32 crop_width = int(image_y * crop_ratio) 33 left = (image_x - crop_width) // 2 34 right = left + crop_width 35 else: 36 # y needs to shrink 37 left = 0 38 right = image_x 39 crop_height = int(image_x * crop_ratio) 40 top = (image_y - crop_height) // 2 41 bottom = top + crop_height 42 43 img = img.crop((left, top, right, bottom)) 44 return img.resize(size, Image.ANTIALIAS) 45 46 47 def squash(thumbnail): 48 """ Resize the image down to exactly `size` (changes ratio) """ 49 img = thumbnail.original_image 50 size = thumbnail.size 51 if img.size[0] < size[0] or img.size[1] < size[1]: 52 raise ThumbnailTooSmall('Image must be at least %s wide and %s high' % size) 53 return img.resize(size, Image.ANTIALIAS) -
django/contrib/thumbnails/templatetags/thumbnails.py
1 from django.template import Library 2 from django.contrib.thumbnails import Thumbnail, crop, squash, ThumbnailException 3 from django.conf import settings 4 from django.utils.html import escape 5 import os.path 6 import re 7 8 register = Library() 9 10 class ThumbnailCrop(Thumbnail): 11 method = crop 12 13 class ThumbnailSquash(Thumbnail): 14 method = squash 15 16 17 #@register.filter 18 def thumbnail(file, size): 19 return create_thumbnail(Thumbnail, file, size) 20 register.filter(thumbnail) 21 22 23 #@register.filter 24 def thumbnail_crop(file, size): 25 return create_thumbnail(ThumbnailCrop, file, size) 26 register.filter(thumbnail_crop) 27 28 29 #@register.filter 30 def thumbnail_squash(file, size): 31 return create_thumbnail(ThumbnailSquash, file, size) 32 register.filter(thumbnail_squash) 33 34 35 #@register.filter 36 def img_tag(thumbnail): 37 if not thumbnail: 38 return '' 39 x, y = thumbnail.thumbnail.size 40 url = escape(thumbnail.url) 41 return '<img src="%s" width="%s" height="%s" />' % (url, x, y) 42 register.filter(img_tag) 43 44 45 re_size_string = re.compile('\d+') 46 def create_thumbnail(thumbnail_cls, file, size_string): 47 """ 48 Creates a thumbnail image for the file (which must exist on MEDIA_ROOT) 49 and returns a url to this image. 50 51 If the thumbnail image is not found, an empty string will be returned. 52 """ 53 # Define the size. 54 bits = [int(bit) for bit in re_size_string.findall(size_string)] 55 if len(bits) == 3: 56 size = bits[:2] 57 jpeg_quality = bits[2] 58 elif len(bits) == 2: 59 size = bits 60 jpeg_quality = None 61 else: 62 return '' 63 64 # Define the filename, then create the thumbnail object. 65 basename, ext = os.path.splitext(file) 66 thumbnail_filename = basename + '_%(x)sx%(y)s_%(method)s_q%(jpeg_quality)s' + ext 67 original_filename = os.path.join(settings.MEDIA_ROOT, file) 68 69 # See if the thumbnail exists already (and is newer than the 70 # original filename). 71 try: 72 thumbnail = thumbnail_cls(thumbnail_filename, size=size, jpeg_quality=jpeg_quality) 73 if os.path.getmtime(original_filename) > os.path.getmtime(thumbnail.filename): 74 thumbnail.delete() 75 else: 76 return thumbnail 77 except (ThumbnailException, OSError): 78 # Couldn't get the thumbnail (or something else went wrong). 79 pass 80 81 # Read the original file from disk. 82 try: 83 data = open(original_filename, 'rb').read() 84 except OSError: 85 # Couldn't read the original file. 86 return '' 87 88 # Generate the thumbnail. 89 try: 90 thumbnail = thumbnail_cls(thumbnail_filename, data, size=size, jpeg_quality=jpeg_quality) 91 except ThumbnailException: 92 return '' 93 94 return thumbnail -
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 ...