An even better Imager macro for Craft CMS
I've been using the same set of macros to transform images with Imager for a while now, it was time to for an update and learn some new Twig tricks along the way.
(In a hurry? Get the entire macro set here. Find out below how this came together.)

What does it need to do?
There were a couple things I wanted the macro collection to do. Of course a regular <img>
tag. But we also want to get a direct link to the image. Lastly srcset
must also be generated when passing multiple transforms.
Lay out some ground rules
The previous set of macros I used for the past years, started to show some flaws. So I created some ground rules that the new set should follow
- Only have 1 place that actually does the transform. In other words, we want to be able to create several macros (one for a regular
img
tag, one withsrcset
, direct url, ... and possible other uses) but have only 1 place where the transform templating code is. - Make sure that there are a couple of defaults in the transform such as sharpening, interlaced, etc.
- These defaults must be overridable per transform, not as a default for each transform. For example in a responsive image we might want to use
crop
mode for our desktop image, but useletterbox
for the mobile image.
Doing the transform
The main macro will be the one that will do the transform with Imager. It'll return either one or multiple url's, either wrapped in src
, width
& height
attributes or not. Let's start with doing some basic checks first:
{% macro src(image, transform, withSizeAttrs = false) %}{% spaceless %}
{% if image %}
{% if image.size|default < craft.app.config.general.maxUploadFileSize %}
...
{% endif %}
{% endif %}
{% endspaceless %}{% endmacro %}
We want to be able to either pass a single transform object, or an array of transforms. These transform(s) must be merged in with our defaults. If there is a single transform, we can do a simple merge:
{% if transform is iterable %}
{# Some good defaults: #}
{% set mergeThisIn = {
position: ((image.focalPoint.x|default(0.5) * 100)) ~ '% ' ~ (image.focalPoint.y|default(0.5) * 100) ~ '%',
effects: { sharpen: true },
interlace: true,
mode: 'crop'
} %}
{% if transform|first is iterable %}
{# This one will be a bit trickier #}
{% else %}
{# Single transform => simple merge #}
{% set transform = mergeThisIn|merge(transform) %}
{% endif %}
{% endif %}
It's not that easy when we're dealing with an array of transforms. Merging in our default parameters in an array with numerical keys isn't possible with Twig, as it will replace the entire object instead of merging it in. We have to loop over the array, update each transform object and then replace the whole array:
...
{% if transform|first is iterable %}
{# This one will be a bit trickier #}
{# Array of transforms (srcset) =>
create a new transform array with the defaults added for each item #}
{% set tempArr = [] %}
{% for i in 0..transform|length-1 %}
{% set tempArr = tempArr|merge({ (i): mergeThisIn|merge(transform[i])}) %}
{% endfor %}
{# We can't update the transform array because
merge doesn't work with numeric keys.. so replace it #}
{% set transform = tempArr %}
{% else %}
...
Now that we have updated the transform(s) with some good defaults, we can create the image(s) and return.
{# Do the transform #}
{% set transformed = craft.imager.transformImage(image, transform) %}
{% if transform|first is not iterable %}
{# Return the transform #}
{{ withSizeAttrs ? 'src="' }}{{ transformed.url }}{{ withSizeAttrs ? '"' }} {{ withSizeAttrs ? "width='#{transformed.width}' height='#{transformed.height}'"|raw }}
{% else %}
{# We can only return strings with macros, this will be transformed into array later on #}
{{ transformed|map(x => "#{x.url} #{x.width}w")|join(',')|raw }}
{% endif %}
You might have noticed something strange about the returned value when dealing with multiple transforms. Macros can only return strings basicly, as explained in this great answer on craftcms.stackexchange.com.
But I want to be able to loop over the generated images. To get this working, we can return a comma separated string and transform it into an array later on.
Using our transform macro
Now that our transform macro is done, let's use it in another macro to return an img
tag.
{% macro regular(image, transform) %}
{% if transform|first is not iterable %}
<img {{ _self.src(image, transform, true) }} alt="{{ (image is iterable) ? image.title : '' }}" />
{% else %}
{# Multiple transforms #}
...
{% endif %}
{% endmacro %}
When we passed in multiple transforms we'll have to do a little more work. We need a single base url for the src
attribute, and we'll need our srcset
string. Remember, we returned a comma separated string in our _self.src()
macro. Converting this back to an array will help us in returning correct markup.
...
{% else %}
{# Multiple transforms #}
{# Do some magic to transform our string into an array #}
{% set helper = create('craft\\helpers\\ArrayHelper') %}
{% set string = _self.src(image, transform) %}
{% set array = helper.toArray(string) %}
{# Get the middle url as default src attribute, and strip out the srcset width descriptor #}
<img src="{{ array[array|length // 2]|split(' ')[0] }}"
srcset="{{ string }}"
alt="{{ (image is iterable) ? image.title : '' }}" {{ attributes|raw }} />
{% endif %}
...
We've set up a solid system to handle our transforms. We can now use our macro to return a single url very easily:
{# Returns a plain url #}
{% macro url(image, transform) %}{% spaceless %}
{{ _self.src(image, transform) }}
{% endspaceless %}{% endmacro %}
What does it look like in use
With our collection of macros in place, lt's time to wrap this up and look at how we can actually use 'em.
{% import '_macros/img-macros' as transform %}
{{ transform.regular(entry.image.one(), { width: 635, height: 476 }) }}
➡️ <img src="..." width="635" height="476" alt="..." />
{# Get a direct url #}
➡️ <div style="background-image: url({{ transform.url(entry.image.one(), { width: 635, height: 476 }) }});"></div>
{{ transform.regular(entry.image.one(), [{ width: 1440 }, { width: 850, height: 476, interlace: false }, { width: 640, height: 640, mode: 'letterbox', letterbox: { color: '#000', opacity: 0 } }]) }}
➡️ <img src="..." srcset="... 1440w, ... 850w, ... 640w" alt="..." >