├── .gitignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── icons ├── logo.svg └── logo2.svg ├── imagefit ├── __init__.py ├── conf.py ├── models.py ├── templatetags │ ├── __init__.py │ └── imagefit.py ├── urls.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | dist/ 4 | *.egg*/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.9" 4 | env: 5 | - DJANGO_VERSION=4.2 6 | install: 7 | - pip install Django==$DJANGO_VERSION 8 | - pip install requests 9 | - pip install Pillow 10 | script: python setup.py test 11 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Main developers: 2 | 3 | Vincent Agnano 4 | 5 | Contributors: 6 | 7 | David Fischer 8 | Sascha Häusler 9 | Dmitriy 10 | Ievgen Pyvovarov 11 | Martin Bachwerk 12 | Sinan Islekdemir 13 | Michael Marx 14 | Marshal Taylor 15 | Oluwafemi Ebenezer 16 | Nebuchadrezzar 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2023 Vincent Agnano. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of django-imagefit nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | deploy: 2 | rm -fr dist/ django_imagefit.egg-info/ 3 | python setup.py sdist 4 | twine upload dist/* 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

django-imagefit

2 | 3 | # Django Image Fit - Resize an image on the fly 4 | 5 | [![Build Status](https://api.travis-ci.org/vinyll/django-imagefit.png)](http://travis-ci.org/vinyll/django-imagefit) 6 | 7 | Imagefit allows you to render an image in a template and specify its dimensions. 8 | It preserves the original image file. 9 | 10 | It is compatible with various sources of images such as django-filebrowser's 11 | FileBrowseField, user uploaded images, static images, … 12 | 13 | Works on Python 3.x; Django 4. Compatible with Django 4.2. 14 | For previous versions of Django please refer to version 0.7.0 and previous versions of Pil. 15 | 16 | 17 | #### Benefits 18 | 19 | * only 1 image file exists on the server, therefore it's always easy to replace 20 | and adapt the image per template or zone. 21 | * no model to adapt for large image and thumbnail that may vary when redesigning 22 | the website. 23 | * perfect match with mediaqueries to adapt on mobile, tablets and 24 | multi-size screens. 25 | * better quality than html/css resizing and no large file download, great for 26 | lower bandwidth. 27 | 28 | 29 | #### Quick tour 30 | 31 | Example 1: render _/static/myimage.png_ image at a maximum size of 200 x 150 px: 32 | 33 | ```html 34 | {{ "/static/myimage.png"|resize:"200x150" }} 35 | ``` 36 | 37 | Example 2: render model's _news.image_ as a thumbnail: 38 | 39 | ```html 40 | {{ news.image|resize:"thumbnail" }} 41 | ``` 42 | 43 | Example 3: render _/static/myimage.png_ image at a maximum cropped size of 150 x 150 px: 44 | 45 | ```html 46 | {{ "/static/myimage.png"|resize:"150x150,C" }} 47 | ``` 48 | 49 | Example 4: render _https://example.com/test.png_ image at a maximum cropped size of 150 x 150 px: 50 | 51 | ```html 52 | {{ "https://example.com/test.png"|external_resize:"150x150,C" }} 53 | ``` 54 | 55 | #### What this is not 56 | 57 | * For creating specific model fields that resize image when model saves, see 58 | [django-imagekit](https://github.com/matthewwithanm/django-imagekit) 59 | * If you wish to avoid very large image on the server, consider resizing your original image 60 | before uploading it 61 | 62 | 63 | ## Installation 64 | 65 | #### Download 66 | 67 | Via pip ![latest version](https://img.shields.io/pypi/v/django-imagefit.svg) 68 | 69 | ```bash 70 | pip install django-imagefit 71 | ``` 72 | 73 | or the bleeding edge version 74 | 75 | ``` 76 | pip install -e git+https://github.com/vinyll/django-imagefit.git#egg=django-imagefit 77 | ``` 78 | 79 | #### update INSTALLED_APPS 80 | 81 | In _settings.py_, add _imagefit_ in your INSTALLED_APPS 82 | 83 | ```python 84 | INSTALLED_APPS = ( 85 | …, 86 | 'imagefit', 87 | ) 88 | ``` 89 | 90 | And add the path relative to your project (see _configuration_ below) 91 | 92 | ```python 93 | IMAGEFIT_ROOT = "public" 94 | ``` 95 | 96 | #### urls.py 97 | 98 | Imagefit is a resize service, therefore include its urls. 99 | 100 | Prefix it with whatever you want (here "imagefit" for example): 101 | 102 | ```python 103 | from django.urls import re_path 104 | 105 | urlpatterns = urlpatterns('', 106 | … 107 | re_path(r'^imagefit/', include('imagefit.urls')), 108 | ) 109 | ``` 110 | 111 | Congratulations, you're all set! 112 | 113 | 114 | ## Usage 115 | 116 | your_template.html 117 | 118 | ```html 119 | {% load imagefit %} 120 | 121 | 122 | 123 | 124 | 125 | ``` 126 | 127 | This will display your _/static/image.png_: 128 | 129 | 1. in the _thumbnail_ format (80 x 80 px) 130 | 2. resized in a custom 320 x 240 pixels 131 | 3. resized and cropped in a custom 320 x 240 pixels 132 | 133 | > the _,C_ modifier stands for _Cropping_ 134 | 135 | ## Configuration 136 | 137 | #### Root path 138 | 139 | You should most probably customize the path to the root folder of your images. 140 | The url your specify in your model will be concatenated to this IMAGEFIT_ROOT 141 | to find the appropriate image on your system. 142 | 143 | The path will be relative to the project folder. 144 | 145 | If starting with a "/", it will be an absolute path (quid about Windows). 146 | 147 | ```python 148 | IMAGEFIT_ROOT = "public" 149 | ``` 150 | 151 | So with this example the image url "/static/image.png" would be pointing to 152 | _/PATH/TO/YOUR/PROJECT/**public/static/image.png**_ 153 | 154 | #### Templatetags 155 | 156 | resize(value, size) # path is relative to you settings.IMAGE_ROOT 157 | static_resize(value, size) # path is relative to you settings.STATIC_ROOT 158 | media_resize(value, size) # path is relative to you settings.MEDIA_ROOT 159 | external_resize(value, size) # path is an http/https url 160 | 161 | Can be used in templates as so : 162 | 163 | {{ "/static/logo.png"|resize:'320x240' }} 164 | {{ "logo.png"|static_resize:'320x240' }} 165 | {{ "user_avatar.png"|media_resize:'320x240' }} 166 | {{ "https://example.com/test.png"|external_resize:'320x240' }} 167 | 168 | 169 | #### Presets 170 | 171 | Presets are configuration names that hold width and height (and maybe more later on). 172 | Imagefit is already shipped with 3 presets : _thumbnail_ (80x80), _medium_ (320x240) 173 | and _original_ (no resizing). 174 | 175 | You may override them or create new ones through settings.py 176 | 177 | 178 | Custom presets examples : 179 | 180 | ```python 181 | IMAGEFIT_PRESETS = { 182 | 'thumbnail': {'width': 64, 'height': 64, 'crop': True}, 183 | 'my_preset1': {'width': 300, 'height': 220}, 184 | 'my_preset2': {'width': 100}, 185 | } 186 | ``` 187 | 188 | 189 | #### Cache 190 | 191 | Because resizing an image on the fly is a big process, django cache is enabled 192 | by default. 193 | 194 | Therefore you are strongly invited to set your imagefit cache preferences to 195 | False for local development. 196 | 197 | You can customize the default cache preferences by overriding default values 198 | described below via settings.py : 199 | 200 | ```python 201 | # enable/disable server cache 202 | IMAGEFIT_CACHE_ENABLED = True 203 | # set the cache name specific to imagefit with the cache dict 204 | IMAGEFIT_CACHE_BACKEND_NAME = 'imagefit' 205 | CACHES = { 206 | 'imagefit': { 207 | 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 208 | 'LOCATION': os.path.join(tempfile.gettempdir(), 'django_imagefit') 209 | } 210 | } 211 | ``` 212 | 213 | Note that `CACHES` default values will be merge with yours from _settings.py_ 214 | 215 | 216 | #### Formats 217 | 218 | Imagefit uses PIL to resize and crop the images and this library requires to 219 | specify the format of the output file. Imagefit allows you to specify an output 220 | format depending of the output filename. Please note that the the output extension 221 | is left unchanged. 222 | 223 | You can customize the default mapping by overriding default values described below 224 | via settings.py : 225 | 226 | ```python 227 | # Example extension -> format. 228 | IMAGEFIT_EXT_TO_FORMAT = {'.jpg': 'jpeg', '.bmp': 'png'} 229 | # Disallow the fall-back to a default format: Raise an exception in such case. 230 | IMAGEFIT_EXT_TO_FORMAT_DEFAULT = None 231 | ``` 232 | 233 | 234 | #### Expires Header 235 | 236 | Django Imagefit comes with Expires header to tell the browser whether it should request the resource from the server or use the cached version. 237 | This has two core benefits. The browser will be using the cached version of the resource in the second load and page load will be much faster. Also, it will require fewer requests to the server. 238 | 239 | As a page score parameter, static resources used in a web page should be containing an Expires information for better performance. 240 | 241 | The default value of the expires header is set to 30 days from now. You can override this value via settings.py as: 242 | 243 | ```python 244 | IMAGEFIT_EXPIRE_HEADER = 3600 # for 1 hour 245 | ``` 246 | 247 | ## Troubleshooting 248 | 249 | 250 | ### "decoder jpeg not available" on Mac OSX 251 | 252 | 253 | You may have installed PIL through pip or easy_install that 254 | does not install libjpeg dependency. 255 | 256 | If so : 257 | 258 | 1. Uninstall pil via pip 259 | 2. Install pip via homebrew: `brew install pil` 260 | 3. Reinstall pil via pip: `pip install pil` 261 | 262 | 263 | ## Todo 264 | 265 | * Refactor _views.resize_ 266 | * Make resize quality/speed configurable 267 | * More examples for doc 268 | * enable URL images in addition to system files 269 | 270 | 271 | ## Imagefit Developers 272 | 273 | _Aka note to self_: Deploy to pypi using `make deploy`. 274 | -------------------------------------------------------------------------------- /icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 10 | 12 | 13 | 15 | 16 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /icons/logo2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 11 | 13 | 15 | 17 | 18 | 21 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /imagefit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinyll/django-imagefit/b97a5bd8ad9a0fcb3eb2bae2f8f267630ec0534c/imagefit/__init__.py -------------------------------------------------------------------------------- /imagefit/conf.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from appconf import AppConf 3 | 4 | import tempfile 5 | import os 6 | if not settings.configured: 7 | settings.configure() 8 | 9 | 10 | class ImagefitConf(AppConf): 11 | 12 | #: dictionary of preset names that have width and height values 13 | IMAGEFIT_PRESETS = { 14 | 'thumbnail': {'width': 80, 'height': 80, 'crop': True}, 15 | 'medium': {'width': 320, 'height': 240}, 16 | 'original': {}, 17 | } 18 | 19 | #: dictionary of output formats depending on the output image extension. 20 | IMAGEFIT_EXT_TO_FORMAT = { 21 | '.jpg': 'jpeg', '.jpeg': 'jpeg' 22 | } 23 | 24 | #: default format for any missing extension in IMAGEFIT_EXT_TO_FORMAT 25 | #: do not fall-back to a default format but raise an exception if set to None 26 | IMAGEFIT_EXT_TO_FORMAT_DEFAULT = 'png' 27 | 28 | #: root path from where to read urls 29 | IMAGEFIT_ROOT = '' 30 | 31 | IMAGEFIT_CACHE_ENABLED = True 32 | IMAGEFIT_CACHE_BACKEND_NAME = 'imagefit' 33 | 34 | settings.CACHES[IMAGEFIT_CACHE_BACKEND_NAME] = { 35 | 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 36 | 'LOCATION': os.path.join(tempfile.gettempdir(), 'django_imagefit') 37 | } 38 | 39 | #: ConditionalGetMiddleware is required for browser caching 40 | # Django's middleware management (and some other things) has changed 41 | # since version 1.9. 42 | property_name = (hasattr(settings, 'MIDDLEWARE') and 'MIDDLEWARE') or 'MIDDLEWARE_CLASSES' 43 | middlewares = getattr(settings, property_name) 44 | if 'django.middleware.http.ConditionalGetMiddleware' not in middlewares: 45 | setattr(settings, property_name, tuple(middlewares) + ('django.middleware.http.ConditionalGetMiddleware',)) 46 | 47 | 48 | def ext_to_format(filename): 49 | extension = os.path.splitext(filename)[1].lower() 50 | format = settings.IMAGEFIT_EXT_TO_FORMAT.get(extension, settings.IMAGEFIT_EXT_TO_FORMAT_DEFAULT) 51 | if not format: 52 | raise KeyError('Unknown image extension: {0}'.format(extension)) 53 | return format 54 | -------------------------------------------------------------------------------- /imagefit/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from imagefit.conf import ext_to_format, settings 3 | from PIL import Image as PilImage 4 | 5 | import mimetypes 6 | import time 7 | import requests 8 | 9 | try: 10 | import BytesIO 11 | except ImportError: 12 | from io import BytesIO 13 | import re 14 | import os 15 | 16 | 17 | class Image(object): 18 | """ 19 | Represents an Image file on the system. 20 | """ 21 | 22 | def __init__(self, path, cache=None, cached_name=None, *args, **kwargs): 23 | self.path = path 24 | self.is_external = path.startswith(('http://', 'https://')) 25 | if self.is_external: 26 | response = requests.get(path) 27 | self.pil = PilImage.open(BytesIO(response.content)) 28 | else: 29 | self.pil = PilImage.open(path) 30 | self.cache = cache 31 | self.cached_name = cached_name 32 | 33 | # force RGB 34 | if self.pil.mode not in ('L', 'RGB', 'LA', 'RGBA'): 35 | self.pil = self.pil.convert('RGB') 36 | 37 | @property 38 | def mimetype(self): 39 | return mimetypes.guess_type(self.path)[0] 40 | 41 | @property 42 | def modified(self): 43 | if self.is_external: 44 | return int(round(time.time())) 45 | return os.path.getmtime(self.path) 46 | 47 | @property 48 | def is_cached(self): 49 | return self.cache and self.cached_name in self.cache 50 | 51 | def resize(self, width=None, height=None): 52 | return self.pil.thumbnail( 53 | (int(width), int(height)), 54 | PilImage.LANCZOS) 55 | 56 | def crop(self, width=None, height=None): 57 | img_w, img_h = self.pil.size 58 | # don't crop an image than is smaller than requested size 59 | if img_w < width and img_h < height: 60 | return self.pil 61 | elif img_w < width: 62 | width = img_w 63 | elif img_h < height: 64 | height = img_h 65 | delta_w = img_w / width 66 | delta_h = img_h / height 67 | delta = delta_w if delta_w < delta_h else delta_h 68 | new_w = img_w / delta 69 | new_h = img_h / delta 70 | self.resize(new_w, new_h) 71 | box_diff = ((new_w - width) / 2, (new_h - height) / 2) 72 | box = ( 73 | int(box_diff[0]), int(box_diff[1]), int(new_w - box_diff[0]), 74 | int(new_h - box_diff[1])) 75 | self.pil = self.pil.crop(box) 76 | return self.pil 77 | 78 | def render(self): 79 | """ 80 | Renders the file content 81 | """ 82 | if self.is_cached: 83 | return self.cache.get(self.cached_name) 84 | else: 85 | image_str = BytesIO() 86 | self.pil.save(image_str, ext_to_format(self.cached_name)) 87 | return image_str.getvalue() 88 | 89 | def save(self): 90 | """ 91 | Save the image to the cache if provided and not cached yet. 92 | """ 93 | if self.cache and not self.is_cached: 94 | image_str = BytesIO() 95 | self.pil.save(image_str, ext_to_format(self.cached_name)) 96 | self.cache.set(self.cached_name, image_str.getvalue()) 97 | image_str.close() 98 | 99 | 100 | class Presets(object): 101 | """ 102 | Representation of an image format storage 103 | """ 104 | 105 | @classmethod 106 | def get_all(cls): 107 | """ 108 | Reads presets from settings 109 | """ 110 | return getattr(settings, 'IMAGEFIT_PRESETS', {}) 111 | 112 | @classmethod 113 | def get(cls, key, to_tuple=False): 114 | """ 115 | Retrieves a specific preset by its name 116 | """ 117 | preset = cls.get_all().get(key, None) 118 | return preset 119 | 120 | @classmethod 121 | def has(cls, key): 122 | """ 123 | Checks if a preset exists 124 | """ 125 | return key in cls.get_all() 126 | 127 | @classmethod 128 | def from_string(cls, string): 129 | """ 130 | Converts a x into a {'width': , 131 | 'height': } dict 132 | return dict or None 133 | """ 134 | if re.match('(\d+)x(\d+),?(\w*)', string): 135 | sizes = [x for x in re.match( 136 | '(\d+)x(\d+)(,?[c|C]?)', string).groups()] 137 | return { 138 | 'width': int(sizes[0]), 'height': int(sizes[1]), 139 | 'crop': bool(sizes[2])} 140 | -------------------------------------------------------------------------------- /imagefit/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinyll/django-imagefit/b97a5bd8ad9a0fcb3eb2bae2f8f267630ec0534c/imagefit/templatetags/__init__.py -------------------------------------------------------------------------------- /imagefit/templatetags/imagefit.py: -------------------------------------------------------------------------------- 1 | from django.template import Library 2 | try: # Django >=1.9 3 | from django.core.urlresolvers import reverse 4 | except ImportError: 5 | from django.urls import reverse 6 | 7 | 8 | register = Library() 9 | 10 | 11 | @register.filter 12 | def resize(value, size, root_name='resize'): 13 | """ 14 | Generates the url for the resized image prefixing with prefix_path 15 | return string url 16 | """ 17 | return reverse('imagefit_resize', kwargs=dict( 18 | path_name=root_name, format=size, url=value)) 19 | 20 | 21 | @register.filter 22 | def media_resize(value, size): 23 | """ 24 | Generates the url for the resized image prefixing with MEDIA_ROOT 25 | return string url 26 | """ 27 | return resize(value, size, 'media_resize') 28 | 29 | 30 | @register.filter 31 | def static_resize(value, size): 32 | """ 33 | Generates the url for the resized image prefixing with STATIC_ROOT 34 | return string url 35 | """ 36 | return resize(value, size, 'static_resize') 37 | 38 | @register.filter 39 | def external_resize(value, size): 40 | """ 41 | Generates the url for the resized image for external image 42 | return string url 43 | """ 44 | return resize(value, size, 'external_resize') 45 | -------------------------------------------------------------------------------- /imagefit/urls.py: -------------------------------------------------------------------------------- 1 | from . import views 2 | 3 | # EAFP compliance with version 4.0 4 | try: 5 | from django.conf.urls import url 6 | except ImportError as e: 7 | from django.urls import re_path 8 | from django.utils import version 9 | 10 | # in case of any other error 11 | if int(version.get_version().split('.')[0]) >= 4: 12 | url = re_path 13 | else: 14 | raise ImportError(str(e)) 15 | 16 | urlpatterns = [ 17 | url( 18 | r'^(?P[\w_-]*)/(?P[,\w-]+)/(?P.*)/?$', 19 | views.resize, 20 | name="imagefit_resize"), 21 | ] 22 | -------------------------------------------------------------------------------- /imagefit/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse, HttpResponseNotModified 2 | from django.core.exceptions import ImproperlyConfigured 3 | from django.core.cache import caches 4 | from django.utils.http import http_date 5 | from django.views.static import was_modified_since 6 | from inspect import signature 7 | 8 | 9 | from imagefit.conf import settings 10 | from imagefit.models import Image, Presets 11 | 12 | import os 13 | import stat 14 | import time 15 | 16 | 17 | cache = caches[settings.IMAGEFIT_CACHE_BACKEND_NAME] 18 | 19 | 20 | def _image_response(image): 21 | response = HttpResponse( 22 | image.render(), 23 | image.mimetype 24 | ) 25 | response['Last-Modified'] = http_date(image.modified) 26 | expire_time = getattr(settings, 'IMAGEFIT_EXPIRE_HEADER', 3600 * 24 * 30) 27 | response['Expires'] = http_date(time.time() + expire_time) 28 | return response 29 | 30 | 31 | def resize(request, path_name, format, url): 32 | if path_name == 'static_resize': 33 | prefix = settings.STATIC_ROOT 34 | elif path_name == 'media_resize': 35 | prefix = settings.MEDIA_ROOT 36 | else: 37 | prefix = settings.IMAGEFIT_ROOT 38 | # remove prepending slash 39 | if url[0] == '/': 40 | url = url[1:] 41 | # generate Image instance 42 | path = os.path.join(prefix, url) 43 | if not os.path.exists(path): 44 | return HttpResponse(status=404) 45 | image = Image(path=path) 46 | statobj = os.stat(image.path) 47 | 48 | # django.views.static.was_modified_since dropped its size argument in 4.1. 49 | sig = signature(was_modified_since) 50 | 51 | if not sig.parameters.get('size'): 52 | if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), 53 | statobj[stat.ST_MTIME]): 54 | return HttpResponseNotModified(content_type=image.mimetype) 55 | 56 | if sig.parameters.get('size'): 57 | if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), 58 | statobj[stat.ST_MTIME], 59 | statobj[stat.ST_SIZE]): 60 | return HttpResponseNotModified(content_type=image.mimetype) 61 | 62 | image.cached_name = request.META.get('PATH_INFO') 63 | 64 | if settings.IMAGEFIT_CACHE_ENABLED: 65 | image.cache = cache 66 | # shortcut everything, render cached version 67 | if image.is_cached: 68 | return _image_response(image) 69 | 70 | # retrieve preset from format argument 71 | preset = Presets.get(format) or Presets.from_string(format) 72 | if not preset: 73 | raise ImproperlyConfigured( 74 | f" \"{format}\" is neither a \"WIDTHxHEIGHT\" format nor a key in " + 75 | "IMAGEFIT_PRESETS." 76 | ) 77 | 78 | # Resize and cache image 79 | if preset.get('crop'): 80 | image.crop(preset.get('width'), preset.get('height')) 81 | else: 82 | image.resize(preset.get('width'), preset.get('height')) 83 | image.save() 84 | 85 | return _image_response(image) 86 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | setup( 7 | name='django-imagefit', 8 | version='0.8.1', 9 | long_description_content_type='text/markdown', 10 | description='Render an optimized version of your original image on display. Ability to resize and crop.', 11 | long_description=open('README.md').read(), 12 | author='Vincent Agnano', 13 | author_email='vinyll@protonmail.com', 14 | url='http://github.com/vinyll/django-imagefit', 15 | license='BSD', 16 | packages=find_packages(), 17 | zip_safe=False, 18 | install_requires=['django-appconf', 'Pillow', 'requests'], 19 | include_package_data=True, 20 | classifiers=[ 21 | 'Environment :: Web Environment', 22 | 'Intended Audience :: Developers', 23 | 'License :: OSI Approved :: BSD License', 24 | 'Operating System :: OS Independent', 25 | 'Programming Language :: Python', 26 | 'Programming Language :: Python :: 2.6', 27 | 'Programming Language :: Python :: 3.3', 28 | 'Topic :: Utilities', 29 | ] 30 | ) 31 | --------------------------------------------------------------------------------