Skip to content

Instantly share code, notes, and snippets.

@coredumperror
Forked from davecranwell/responsive_image.py
Last active October 17, 2023 15:59
Show Gist options
  • Save coredumperror/41f9f8fe511ac4e88547487d6d43c69b to your computer and use it in GitHub Desktop.
Save coredumperror/41f9f8fe511ac4e88547487d6d43c69b to your computer and use it in GitHub Desktop.
Responsive image tag for Wagtail CMS
from django import template
from wagtail.wagtailimages.models import SourceImageIOError
from wagtail.wagtailimages.templatetags.wagtailimages_tags import ImageNode
register = template.Library()
@register.tag(name="responsiveimage")
def responsiveimage(parser, token):
bits = token.split_contents()[1:]
image_expr = parser.compile_filter(bits[0])
filter_spec = bits[1]
remaining_bits = bits[2:]
if remaining_bits[-2] == 'as':
attrs = _parse_attrs(remaining_bits[:-2])
# token is of the form {% responsiveimage self.photo max-320x200 srcset="filter_spec xyzw" [ attr="val" ... ] as img %}
return ResponsiveImageNode(image_expr, filter_spec, attrs=attrs, output_var_name=remaining_bits[-1])
else:
# token is of the form {% responsiveimage self.photo max-320x200 srcset="filter_spec xyzw" [ attr="val" ... ] %}
# all additional tokens should be kwargs, which become attributes
attrs = _parse_attrs(remaining_bits)
return ResponsiveImageNode(image_expr, filter_spec, attrs=attrs)
def _parse_attrs(bits):
template_syntax_error_message = (
'"responsiveimage" tag should be of the form '
'{% responsiveimage self.photo max-320x200 srcset="fill-400x120 400w, fill-600x180 600w" sizes="100vw" [ custom-attr="value" ... ] %} or '
'{% responsiveimage self.photo max-320x200 srcset="whatever" as img %}'
)
attrs = {}
for bit in bits:
try:
name, value = bit.split('=')
except ValueError:
raise template.TemplateSyntaxError(template_syntax_error_message)
if value[0] == value[-1] and value[0] in ('"', "'"):
# If attribute value is in quotes, strip the quotes and store the attr as a string.
attrs[name] = value[1:-1]
else:
# This attribute isn't in quotes, so it's a variable name. Send a Variable as the attr, so the
# ResponsiveImageNode can render it based on the context it gets.
attrs[name] = template.Variable(value)
return attrs
class ResponsiveImageNode(ImageNode, template.Node):
def render(self, context):
try:
image = self.image_expr.resolve(context)
except template.VariableDoesNotExist:
return ''
if not image:
return ''
try:
rendition = image.get_rendition(self.filter)
except SourceImageIOError:
# It's fairly routine for people to pull down remote databases to their
# local dev versions without retrieving the corresponding image files.
# In such a case, we would get a SourceImageIOError at the point where we try to
# create the resized version of a non-existent image. Since this is a
# bit catastrophic for a missing image, we'll substitute a dummy
# Rendition object so that we just output a broken link instead.
Rendition = image.renditions.model # pick up any custom Image / Rendition classes that may be in use
rendition = Rendition(image=image, width=0, height=0)
rendition.file.name = 'not-found'
# Parse srcset format into array of renditions.
try:
try:
# Assume it's a Variable object, and try to resolve it against the context.
srcset = self.attrs['srcset'].resolve(context)
except AttributeError:
# It's not a Variable, so assume it's a string.
srcset = self.attrs['srcset']
# Parse each src from the srcset.
raw_sources = srcset.replace('"', '').split(',')
srcset_renditions = []
widths = []
newsrcseturls = []
for source in raw_sources:
flt = source.strip().split(' ')[0]
width = source.strip().split(' ')[1]
# cache widths to be re-appended after filter has been converted to URL
widths.append(width)
try:
srcset_renditions.append(image.get_rendition(flt))
except SourceImageIOError:
# pick up any custom Image / Rendition classes that may be in use
TmpRendition = image.renditions.model
tmprend = TmpRendition(image=image, width=0, height=0)
tmprend.file.name = 'not-found'
for index, rend in enumerate(srcset_renditions):
newsrcseturls.append(' '.join([rend.url, widths[index]]))
except KeyError:
newsrcseturls = []
pass
if self.output_var_name:
rendition.srcset = ', '.join(newsrcseturls)
# return the rendition object in the given variable
context[self.output_var_name] = rendition
return ''
else:
# render the rendition's image tag now
resolved_attrs = {}
for key in self.attrs:
if key == 'srcset':
resolved_attrs[key] = ','.join(newsrcseturls)
continue
try:
# Assume it's a Variable object, and try to resolve it against the context.
resolved_attrs[key] = self.attrs[key].resolve(context)
except AttributeError:
# It's not a Variable, so assume it's a string.
resolved_attrs[key] = self.attrs[key]
return rendition.img_tag(resolved_attrs)
@elmcrest
Copy link

👍 thx

@bpb54321
Copy link

bpb54321 commented Oct 29, 2018

Works great! One update, though: with Wagtail 2.3, need to change to imports to read:

from wagtail.images.models import SourceImageIOError
from wagtail.images.templatetags.wagtailimages_tags import ImageNode

@helenb
Copy link

helenb commented Apr 25, 2019

One issue with this code is that it renders the final image with the width and height attributes included. I'm pretty sure that this is not appropriate for images with srcset.

@thibaudcolas
Copy link

Just noting this is now officially supported in Wagtail, to be released in Wagtail 5.2

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