Skip to content

Instantly share code, notes, and snippets.

@gregplaysguitar
Last active June 23, 2023 21:46
Show Gist options
  • Save gregplaysguitar/6cc925abb421fd9783f4456cc5babf41 to your computer and use it in GitHub Desktop.
Save gregplaysguitar/6cc925abb421fd9783f4456cc5babf41 to your computer and use it in GitHub Desktop.
Auto-generate srcset and sizes attributes for an <img> with Django & sorl-thumbnail

srcset.py

Auto-generate srcset and sizes attributes for an image in Django. This script makes a few assumptions about your site structure and uses them to create srcset and sizes attributes for an <img> tag.

Assumptions

  • At a browser width of DESIGN_WIDTH, the image width will match the value passed to srcset
  • At a browser widths between MOBILE_MAX and MAX_WIDTH, the image will resize proportionally
  • At browser widths above MAX_WIDTH, the image size will be proportional to MAX_WIDTH
  • At a browser widths below MOBILE_MAX, all images render at 100% width

Requirements

Either easy_thumbnails or sorl-thumbnail is required.

Configuration

The following can be configured in your django settings (default values shown)

SRCSET_REFERENCE_WIDTH = 1440
SRCSET_DESKTOP_MAX = 2000
SRCSET_MOBILE_MAX = 767
SRCSET_MOBILE_MIN = 375

Example usage

In a jinja2 template, if sorl.thumbnail.get_thumbnail and srcset.srcset are exposed as global variables, and obj.image is a django ImageField then the following template code:

{% set thumb = get_thumbnail(obj.image, '1020x816', crop='center') %}
<img src="{{ thumb.url }}" {{ srcset(image, '1020x816', crop='center') }}>

will result in something like

<img src="/media/../1020x816.jpg"
     srcset="/media/../image-1020x816.jpg 1020w,
             /media/../image-2040x1632.jpg 2040w,
             /media/../image-750x600.jpg 750w"
     sizes="(max-width: 767px) 100%,
            (min-width: 768px and max-width: 2000px) 70.83%,
            (min-width: 2001px) 1417px">
Copyright (c) 2015 Greg Brown
[gregbrown.co.nz](http://gregbrown.co.nz)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
try:
import easy_thumbnails.files
easy = True
except ImportError:
import sorl.thumbnail
easy = False
try:
from jinja2 import Markup
except ImportError:
def Markup(s):
return s
from django.conf import settings
REFERENCE_WIDTH = getattr(settings, 'SRCSET_REFERENCE_WIDTH', 1440)
DESKTOP_MAX = getattr(settings, 'SRCSET_DESKTOP_MAX', 2000)
MOBILE_MAX = getattr(settings, 'SRCSET_MOBILE_MAX', 767)
MOBILE_MIN = getattr(settings, 'SRCSET_MOBILE_MIN', 375)
def thumbnail(image, size, crop):
if easy:
thumbnailer = easy_thumbnails.files.get_thumbnailer(image)
return thumbnailer.get_thumbnail({
'size': size,
'crop': crop,
})
else:
return sorl.thumbnail.get_thumbnail(image, 'x'.join(size), crop)
def generate_srcset(image, size, crop=None, retina=2):
"""Generate srcset and sizes properties for an <img>. image should be
a django image object, i.e. an instance of models.ImageField.
size should be a (W, H) tuple or a WxH string. Returns srcset and sizes
attributes for an <img> tag, wrapped in jinja2 Markup if available.
Assumes the following:
- the width component of size equals the img element's pixel width at
browser size REFERENCE_WIDTH
- between MOBILE_MAX and REFERENCE_WIDTH, the img resizes proportionally
- below MOBILE_MAX, the img is 100% of the browser width
- if image is not cropped, the thumbnail will be constrained by width,
not height
"""
if not image:
return ''
if isinstance(size, str):
size = tuple(map(int, size.split('x')))
w, h = size
# standard size
size_pairs = [(w, h)]
# add retina thumbnail to srcset, but only if the source image is big
# enough to warrant it
if retina:
retina_size = tuple(map(int, (w * retina, h * retina)))
w_bigger = image.width > retina_size[0]
h_bigger = image.height > retina_size[1]
if w_bigger and h_bigger or not crop and (w_bigger or h_bigger):
size_pairs.append(retina_size)
# add mobile size if the original image is bigger, and if it's different
# enough to be worth it
mobile_w = MOBILE_MIN * 2
if w > mobile_w * 1.2:
size_pairs.append((mobile_w, int(mobile_w * h / w)))
# no srcset required if there's only one size
if len(size_pairs) <= 1:
return ''
imgs = (thumbnail(image, s, crop=crop) for s in size_pairs)
srcset = ', '.join('%s %sw' % (img.url, img.width) for img in imgs)
# generate sizes - 100% below MOBILE_MAX, a pixel width above DESKTOP_MAX,
# and a percentage in between
# TODO account for actual resized image discrepancies here
size_data = [
# min, max, image size
(None, MOBILE_MAX, '100vw'),
(MOBILE_MAX + 1, DESKTOP_MAX,
'%svw' % (round(float(w) / REFERENCE_WIDTH * 100, 2))),
(DESKTOP_MAX + 1, None, '%spx' % (w * DESKTOP_MAX / REFERENCE_WIDTH))
]
def make_size(data):
min_px, max_px, size = data
mq_list = []
if min_px:
mq_list.append('min-width: %spx' % min_px)
if max_px:
mq_list.append('max-width: %spx' % max_px)
return '(%s) %s' % (') and ('.join(mq_list), size)
sizes = ', '.join(map(make_size, size_data))
return Markup('srcset="%s" sizes="%s"' % (srcset, sizes))
@sahaliyev
Copy link

please explain more, where should I put this code. Usage part is unclear

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment