I want to be able to upload a single high resolution image, and output a <picture>
element that outputs it with a few different sizes and resolutions automatically (using Picturefill to sort out which one actually shows up to the browsers). How can I do that?
3 Answers
You can do this using image transforms.
There are a couple things you’re going to need to consider though:
- Not every image will necessarily be high-res (e.g. a computer screenshot taken on a non-retina screen)
- You probably don’t want to output images that are larger than their original size
For #1, we need some way of identifying images that are 1X vs. 2X. You could do this a couple different ways:
- Give your assets a Lightswitch field that identifies if it’s a 2X image
- Use a consistent naming convention for 2X image filenames, e.g. “[email protected]” (to borrow Retina.js’s convention)
I personally prefer the second. (That’s what we do on buildwithcraft.com, except we use “.2x” instead of “@2x”.)
We can tackle #2 with some logic in the template that only uses a transform when the resulting size is going to be smaller than the original image.
It’s the combination of these two problems where things get interesting: You need to figure out the image’s CSS pixel size (based on whether it’s 1X or 2X) before you start determining whether it needs a transform, and how big to make the transform, etc.
Here’s what we came up with for Craft’s documentation (such as the image on the asset transforms page):
- Create a macro called
srcset()
which will be responsible for outputting a<source>
or<img>
tag’ssrcset=
attribute and value, and optionally thewidth=
andheight=
attributes as well. It should accept the following arguments:image
- an AssetFileModelmaxCssWidth
- the maximum “CSS pixel” width that we want to output the image atincludeDimensions
- whether the macro should also output thewidth=
andheight=
attributes.
- Create a
<picture>
element with nested<source>
and<img>
elements per the Picture spec, using our newsrcset()
macro to write out thesrcset=
attributes.
Here’s what the macro looks like:
{% macro srcset(image, maxCssWidth, includeDimensions) %}
{%- spaceless %}
{# Is this a 2X image? #}
{% set is2x = image.filename matches '/@2x\\.\\w+$/' %}
{# Determine the CSS width for the image, if it were output at 100% #}
{% set cssWidth = (is2x ? round(image.width / 2) : image.width) %}
{# Determine the image size that 1X screens should get #}
{% set width = (cssWidth > maxCssWidth ? maxCssWidth : cssWidth) %}
{# Determine the image size that 2X screens should get #}
{% set width2x = (width * 2) > image.width ? image.width : (width * 2) %}
{# Output the srcset= attribute value #}
srcset="{{ image.getUrl(image.width != width ? { width: width }) }}, {{ image.getUrl(image.width != width2x ? { width: width2x }) }} 2x"
{#- Output the width= and height= attributes if needed #}
{%- if includeDimensions %} width="{{ width }}" height="{{ image.getHeight({ width: width }) }}"{% endif %}
{% endspaceless -%}
{% endmacro %}
Adjust that {% set is2x = ...
line based on however you’re identifying 2X images. For example if you’re going the Lightswitch route instead, it could be as simple as:
{% set is2x = image.is2x %}
Note that each of the image.getUrl()
calls on the {{ output }} line are making sure that the width is different than the actual image width before passing the { width: X }
object into getUrl()
- that way we’re not having Craft go through the trouble of generating a transform unless we actually need to.
With that macro in place, now we just need to import it in our template, and start using it.
{% from _self import srcset %}
{# ... #}
{% for image in entry.myAssetsField %}
<picture>
<!--[if IE 9]><video style="display: none;"><![endif]-->
<source media="(min-width: 1000px)" {{ srcset(image, 1000) }}>
<source media="(min-width: 800px)" {{ srcset(image, 800) }}>
<source media="(max-width: 320px)" {{ srcset(image, 320) }}>
<!--[if IE 9]></video><![endif]-->
<img {{ srcset(image, 600, true) }} alt="{{ image.title }}">
</picture>
{% endfor %}
If you placed the macro in a different template, replace _self
with the path to that template, e.g. "_macros"
.
-
2Quick note: I think a picture element is overkill in this use case.
srcset
andsizes
attributes on animg
tag would be enough. Commented Aug 25, 2014 at 8:20 -
1Using a
<picture>
tag would sort of indicate art direction or a ratio change. Which is in plugin territory. See graphics8.nytimes.com/images/2014/05/22/blogs/imagecrop480.gif for a good demo taken from open.blogs.nytimes.com/2014/06/17/… With responsive design becoming the norm it wouldn't be the worst thing to find out of the box.– jnowlandCommented Aug 28, 2014 at 0:45
Quick comment:
If you are using generated transforms, you are likely not doing any "art direction" with your images. Basically all your images will be the same, their dimensions alone would be different.
If you are not concerned about art direction, a picture element is overkill and you should just look at srcset
and sizes
attributes on an image tag. Yoav Weiss explains it better than I ever could. Eric Portis also has a comprehensive blog post on srcset
and sizes
.
<img src="small.jpg"
srcset="large.jpg 1024w,
medium.jpg 600w,
small.jpg 350w"
sizes="(min-width: 64em) 33.3vw,
(min-width: 37.5em) 50vw,
100vw"
alt="alternate text" />
Using this, the browser will figure out which image to serve, depending on the screen size and pixel resolution. Magic.
PS: To make sure all the transforms are generated by Craft with picturefill, use 'generateTransformsBeforePageLoad' => true
in your general.php config file.
-
2
-
Great news. I figured as much since you are using Picturefill on buildwithcraft ;o) Commented Aug 26, 2014 at 12:35
There is a great article here which I thoroughly recommend reading. It goes into this in great detail.
It even includes a twig macro that deals with both retina images as well as responsive images:
https://straightupcraft.com/articles/responsive-images-with-twig-macros