24

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?

38

You can do this using image transforms.

There are a couple things you’re going to need to consider though:

  1. Not every image will necessarily be high-res (e.g. a computer screenshot taken on a non-retina screen)
  2. 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. “filename@2x.ext” (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):

  1. Create a macro called srcset() which will be responsible for outputting a <source> or <img> tag’s srcset= attribute and value, and optionally the width= and height= attributes as well. It should accept the following arguments:
    • image - an AssetFileModel
    • maxCssWidth - the maximum “CSS pixel” width that we want to output the image at
    • includeDimensions - whether the macro should also output the width= and height= attributes.
  2. Create a <picture> element with nested <source> and <img> elements per the Picture spec, using our new srcset() macro to write out the srcset= 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".

10

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
    That setting won’t be necessary in 2.2 ;) – Brandon Kelly Aug 25 '14 at 16:15
  • Great news. I figured as much since you are using Picturefill on buildwithcraft ;o) – Jérôme Coupé Aug 26 '14 at 12:35
7

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.