Code

Ticket #4115: contrib.thumbnails.2.patch

File contrib.thumbnails.2.patch, 20.7 KB (added by SmileyChris, 7 years ago)

Next cut

  • django/contrib/thumbnails/base.py

     
     1from django.conf import settings 
     2import os 
     3from StringIO import StringIO 
     4from PIL import Image 
     5from exceptions import ThumbnailNoData, ThumbnailInvalidImage 
     6from methods import scale 
     7 
     8__all__ = ('Thumbnail',) 
     9 
     10class 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

     
     1from base import * 
     2from exceptions import * 
     3from methods import * 
  • django/contrib/thumbnails/exceptions.py

     
     1class ThumbnailException(Exception): 
     2    pass 
     3 
     4class ThumbnailNoData(ThumbnailException): 
     5    pass 
     6 
     7class ThumbnailTooSmall(ThumbnailException): 
     8    pass 
     9 
     10class ThumbnailInvalidImage(ThumbnailException): 
     11    pass 
  • django/contrib/thumbnails/methods.py

     
     1from PIL import Image 
     2from exceptions import ThumbnailTooSmall 
     3 
     4 
     5def 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 
     15def 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 
     47def 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

     
     1from django.template import Library 
     2from django.contrib.thumbnails import Thumbnail, crop, squash, ThumbnailException 
     3from django.conf import settings 
     4from django.utils.html import escape 
     5import os.path 
     6import re 
     7 
     8register = Library() 
     9 
     10class ThumbnailCrop(Thumbnail): 
     11    method = crop 
     12 
     13class ThumbnailSquash(Thumbnail): 
     14    method = squash 
     15 
     16 
     17#@register.filter 
     18def thumbnail(file, size): 
     19    return create_thumbnail(Thumbnail, file, size) 
     20register.filter(thumbnail) 
     21 
     22 
     23#@register.filter 
     24def thumbnail_crop(file, size): 
     25    return create_thumbnail(ThumbnailCrop, file, size) 
     26register.filter(thumbnail_crop) 
     27 
     28 
     29#@register.filter 
     30def thumbnail_squash(file, size): 
     31    return create_thumbnail(ThumbnailSquash, file, size) 
     32register.filter(thumbnail_squash) 
     33 
     34 
     35#@register.filter 
     36def 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) 
     42register.filter(img_tag) 
     43 
     44 
     45re_size_string = re.compile('\d+') 
     46def 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========================= 
     2django.contrib.thumbnails 
     3========================= 
     4 
     5The ``django.contrib.thumbnails`` package, part of the `"django.contrib" add-ons`_, 
     6provides a way of thumbnailing images. 
     7 
     8It requires the Python Imaging Library (PIL_). 
     9 
     10.. _"django.contrib" add-ons: ../add_ons/ 
     11.. _PIL: http://www.pythonware.com/products/pil/ 
     12 
     13Template filters 
     14================ 
     15 
     16To 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 
     19filters. 
     20 
     21The thumbnail creation filters, all very similar in behaviour, are: 
     22 
     23 * ``thumbnail`` 
     24 * ``thumbnail_crop`` 
     25 * ``thumbnail_squash`` 
     26 
     27The only difference between them is the `Thumbnail methods`_ that they use. 
     28 
     29One other filter is provided as a helper to the most common use: 
     30 
     31 * ``img_tag`` 
     32 
     33Using the thumbnail filters 
     34--------------------------- 
     35 
     36Usage:: 
     37 
     38    <img src="{{ object.imagefield|thumbnail:"150x100" }}" /> 
     39 
     40The 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 
     43down 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 
     45and returns the URL to this thumbnail image. 
     46 
     47The ``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 
     52If the thumbnail filename already exists, it is only overwritten if the date of 
     53the the original image file is newer than the thumbnail file. 
     54 
     55The ``q75`` refers to the JPEG quality of the thumbnail image. You can change 
     56the quality by providing a third number to the filter:: 
     57 
     58    {{ object.imagefield|thumbnail:"150x10 85" }} 
     59 
     60Rather than just outputting the url, you can reference any other properties 
     61(see the ```Thumbnail`` object properties`_ section below for a complete 
     62list):: 
     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 
     68The above example is the most common case, and the ``img_tag`` filter is 
     69provided to make that easier. The following example explains it's use:: 
     70 
     71        {{ object.imagefield|thumbnail:"150x100"|img_tag }} 
     72 
     73Creating a thumbnail 
     74==================== 
     75 
     76The rest of this documentation deals with lower-level usage of thumbnails. 
     77 
     78To 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 
     83The 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 
     122The thumbnail object which is created provides the following properties and 
     123functions: 
     124 
     125``filename`` 
     126------------ 
     127 
     128Reading this property returns the full path and filename to this thumbnail 
     129image. 
     130 
     131When you set this property, the filename string you provide is internally 
     132appended to the ``Thumbnail`` object's ``root`` property. 
     133 
     134You can use string formatting to generate the filename based on the 
     135thumbnailing 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 
     142For 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 
     148Note: thumbnailed images are always saved as JPEG images, so if the filename 
     149string does not end in `'.jpg'`, this will be automatically appended to the 
     150thumbnail's filename. 
     151 
     152``original_image`` 
     153------------------ 
     154 
     155This read-only property returns a PIL ``Image`` containing the original 
     156image (passed in with ``data``). 
     157 
     158``thumbnail`` 
     159------------- 
     160 
     161This read-only property returns a PIL ``Image`` containing the thumbnail 
     162image. 
     163 
     164``url`` 
     165------- 
     166 
     167This read-only property returns the full url this thumbnail image. 
     168 
     169It is generated by appending the parsed ``filename`` string to the  
     170``Template`` object's ``base_url`` property. 
     171 
     172``delete()`` 
     173------------ 
     174 
     175Call this function to delete the thumbnail file if it exists on the disk. 
     176 
     177Custom thumbnails 
     178================= 
     179 
     180Similar to newforms, you can create a subclass to override the default 
     181properties of the ``Thumbnail`` base class:: 
     182 
     183    from django.contrib.thumbnails import Thumbnail 
     184 
     185    class MyThumbnail(Thumbnail): 
     186        size = (100, 100) 
     187 
     188Here 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 
     201Thumbnail methods 
     202================= 
     203 
     204There are several thumbnailing methods available in 
     205``django.contrib.thumbnails.methods`` 
     206 
     207``crop()`` 
     208---------- 
     209 
     210This 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 
     213It requires the original image to be both as wide and as high as ``size``. 
     214 
     215``scale()`` 
     216----------- 
     217 
     218This is the normal PIL scaling method of proportionally resizing the image down 
     219to no greater than the thumbnail ``size`` dimensions. 
     220 
     221It requires the original image to be either as wide or as high as ``size``. 
     222 
     223``squash()`` 
     224------------ 
     225 
     226This method resizes the image down to exactly the dimensions given. This will 
     227potentially squash or stretch the image. 
     228 
     229It requires the original image to be both as wide and as high as ``size``. 
     230 
     231Making your own methods 
     232----------------------- 
     233 
     234To make your own thumbnailing function, create a function which accepts one 
     235parameter (``thumbnail``) and returns a PIL ``Image``. 
     236 
     237The ``thumbnail`` parameter will be a ``Thumbnail`` object, so you can use it 
     238to get the original image (it will raise ``ThumbnailNoData`` if no data was 
     239provided) and the thumbnail size:: 
     240 
     241    img = thumbnail.original_image 
     242    size = thumbnail.size 
     243 
     244Exceptions 
     245========== 
     246 
     247The following exceptions (all found in ``django.contrib.thumbnails.exceptions`` 
     248and 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 
     267Putting it all together 
     268======================= 
     269 
     270Here is a snippet of an example view which receives an image file from the user 
     271and 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        ...