Introducing:
The Power Pack and Quick Transform Syntax

Increase your developer productivity with more power tools for responsive images.

— Published on April 19, 2024

After using Imager for eight years, I had two insights ("About time!", some would say):

  1. 95% of the transforms I do are from one width to another, maybe in a given aspect ratio, and maybe in a specific format (ie, webp/avif).
  2. I'm so very, very tired of creating different macros, includes and helper functions for creating <picture> tags.

Although fillTransforms has been around for a long time, helping make things more efficient in terms of number 1, the legacy syntax started to feel too verbose. And although I'd sworn an oath never to create a plugin that outputs markup ("It's not the Craft way!", I yelled into the void), I decided it was time to try and solve number 2 once and for all.

The Power Pack was born! 🥷💥

And pretty quickly I realised that the most essential part of what I was building, should actually be a part of the core Imager plugin instead...

The Quick Syntax was born! 🏎️💨

The Quick Syntax

As I said, the normal/legacy syntax started to feel too verbose:

craft.imagerx.transformImage(image, [{ width: 600 }, { width: 1800 }], { ratio: 16/9, format: 'webp' }, { fillTransforms: true })

That doesn't exactly roll off the tongue (although fillTransforms should probably be in your default config).

This on the other hand:

craft.imagerx.transformImage(image, [600, 1800, 16/9, 'webp'])

The shortest possible way to express minimum width, maximum width, ratio and format! To make it even more flexible, the third parameter can either be a ratio, like in the example, a file format, or an object with default transform parameters.

craft.imagerx.transformImage(image, [600, 1800, 16/9, 'webp'])
craft.imagerx.transformImage(image, [600, 1800, 'webp']) 
craft.imagerx.transformImage(image, [600, 1800, { effects: { grayscale: true } }]) 
craft.imagerx.transformImage(image, [600, 1800, { ratio: 16/9, interlaced: true }, 'jpg'])

All of these are valid!

Although fillTransforms have served its purpose since it was added, it always felt a bit awkward. Having to define the interval between the transforms in pixels with fillInterval, made it a little bit static and difficult to use consistently throughout your site. With the new quick syntax it felt like something that needed improvements, since it relies completely on filling in the transforms between minimum and maximum width.

The solution is that fillTransforms can now be set to 'auto', which the quick syntax will automatically use. This will make a new config setting, autoFillCount, go into effect, which controls the number of transforms between minimum and maximum with that should be created. The default value is 3, which makes the total number of transforms be 5. So in the example above, the image will be transformed to 600, 900, 1200, 1500 and 1800 pixels automatically.

autoFillCount also takes a value of 'auto', which makes the fill count relative to the difference between minimum and maximum width.

imager-x.php:

<?php 
return [
    'fillTransforms' => 'auto',
    'autoFillCount' => 'auto'
];

Hot sauce right here!

Since the quick syntax is now a core part of Imager, it can also be used directly inside named transforms and auto generate configs.

imager-x-transforms.php:

<?php
return [
    'heroImage' => [
        'displayName' => 'Hero image',
        'transforms' => [800, 2000, 16/9],
    ],
    'heroImageWebp' => [
        'displayName' => 'Hero image',
        'transforms' => [800, 2000, 16/9, 'webp'],
    ],
];

imager-x-generate.php:

<?php

return [
    'elements' => [
        [
            'elementType' =>  \craft\elements\Entry::class,
            'criteria' => [
                'section' => ['news'],
            ],
            'fields' => ['heroImage'],
            'transforms' => [
                [800, 2000, 16/9],
                [800, 2000, 16/9, 'webp'], 
            ]
        ],
        
    ],
    'fields' => [
        'image' => [
            [800, 2000],    
            [800, 2000, 16/9],
            'myNamedTransform',
            [600, 1200, 3/4, 'webp'],    
            [600, 600, 3/4, 'jpg'],    
        ]
    ],
];

The Power Pack

With that in place, it's time to look at the power pack itself. It's a stand alone plugin, and essentially a couple of twig functions that helps you generate <picture> and <img> elements quickly, with all the best practices taken care of.

Let's look at a common use-case; you want to create a responsive full-width hero image. Below 750px browser width it should have a 1:1 ratio, and above you want it to have a 16:9 ratio. Using the pppicture template function, we can do the following:

{# Template code #}
{{ pppicture(
    [
        [image, [1200, 2000, 16/9], 750],
        [image, [800, 1600, 1]]
    ]
) }}

{# Resulting markup #}
<picture>
    <source srcset="..." 
        sizes="100vw"
        width="1200" height="675" 
        media="(min-width: 768px)"> 
    <img src="..." srcset="..." 
        sizes="100vw" 
        width="800" height="800" 
        alt="The quick brown fox jumps" 
        loading="lazy" decoding="auto" 
        style="object-position: 20% 40%;">
</picture>

A valid <picture> element has been created, with two source sets. Width and height attributes are added based on the transforms, alt text have been pulled in from the asset in Craft, best practice settings for loading and decoding is used, and the object position CSS style has been added based on the focal point from the asset (this is automatically factored in by Imager when doing the transform).

But what if you don't use the default alt field in Craft? Or want to eager load images by default? No problem, almost everything is configurable, take a look at the documentation.

Let's build on the example above and add support for delivering WebP images to browsers that support it:

{# Template code #}
{{ pppicture(
    [
        [image, [1200, 2000, 16/9, 'webp'], 750, 'webp'],
        [image, [1200, 2000, 16/9, 'jpg'], 750],
        [imageMobile, [800, 1600, 1, 'webp'], 'webp'],
        [imageMobile, [800, 1600, 1, 'jpg']]
    ]
) }}

{# Resulting markup #}
<picture>
    <source srcset="..." media="(min-width: 768px)" type="image/webp" sizes="100vw" width="1200" height="675">
    <source srcset="..." media="(min-width: 768px)" sizes="100vw" width="1200" height="675">
    <source srcset="..." type="image/webp" sizes="100vw" width="800" height="800">
    <img src="..." srcset="..." sizes="100vw" loading="lazy" decoding="auto" style="object-position: 50% 50%;" width="800" height="800" alt="The quick brown fox jumps">
</picture>

Now we're really starting to save some time and reducing our cognitive load!

Let's quickly have a look at another common use-case; you simply just want to deliver WebP images as an alternative to browsers that support it:

{{ pppicture(
    [
        [image, [800, 2000, 'webp'], 'webp'],
        [image, [800, 2000]]
    ]
) }}

That was almost too easy!

So far we've relied on a lot of defaults, but let's go bananas and override things:

{{ pppicture(
    [
        [image, [1000, 2000, 16/9], 768],
        [imageMobile, [500, 1000, 1]
    ],
    {
        sizes: '(min-width: 768px) 50vw, 100vw',
        class: 'absolute full',
        loading: 'eager',
        decoding: 'async',
        alt: 'This is a custom alternative text!',
        defaults: { effects: { sharpen: true } },
        imagerOverrides: {
            transformer: 'craft'
        }
    }, 
    {
        lazysizes: true,
        placeholder: 'blurhash'
    }) }}

A lot going on here, but hopefully it should be easy enough to understand. The second parameter are various parameters that is used by the function, and the third one is config overrides.

Most notably there is sizes, which you will have to supply in order for the browser to know what the intended size of the image should be. It defaults to 100vw by default, which is fine for full width images, but make sure you customize this for other things. Unless you use (the awesome) lazysizes, in which case it will be set to auto, and you'll never have to think about it again.

The class parameter is for adding classes to the <img> tag, both for the normal one inside the <picture>, but also the fallback image inside the <noscript> tag if you're using lazysizes. If you want to add classes or other attributes to the picture tag itself, use the native attr filter on the output directly.

ppimg

In addition to pppicture, there's the simpler ppimg which only outputs a single <img> element (Actually, it's just a special call to pppicture with one source). Here's what it looks like:

{# Template code #}
{{ ppimg(image, [1000, 2000, 16/9]) }}

{# Resulting markup #}
<img src="..." srcset="..." 
    sizes="100vw"
    width="1000" height="563" 
    alt="" 
    loading="lazy" decoding="auto" 
    style="object-position: 50% 50%;">

And it takes the same parameters and config overrides as pppicture.

{{ 
    ppimg(image, 'myNamedTransform', {
        sizes: '(min-width: 1024px) calc(100vw-80px), calc(100vw-40px)'
        class: 'absolute full',
        loading: 'eager'
    }, {
        lazysizes: true,
        placeholder: 'blurhash'
    }) 
}}

ppplaceholder

If you use the placeholder functionality in the config, a placeholder is added as a background on the <img> element. If you want the placeholder to be on the wrapping element instead, for instance if you want a smooth transition to the real image when it loads, you can use ppplaceholder.

<div class="relative w-full h-0 pb-[56.25%]" {{ ppplaceholder(image, 'attr', 'blurup') }}>
    {{ 
        ppimg(image, [1000, 2000, 16/9], { class: 'absolute full' })
    }}
</div>

pptransform

And finally, a small timesaver, pptransform, that's just a wrapper around craft.imagerx.transformImage, because that became tedious to write. And with the added benefit that it will respect the transformSvgs and transformAnimatedGifs config settings.

{% set transforms = pptransform(image, [1000, 2000]) %}
{{ transforms|srcset }}

And that's that!

Imager has always been about providing developers with an abundance of tools to make image transforms, manipulations and analysis as easy and efficient as possible. The new quick syntax and the power pack will save you even more time, and level up your responsive images.

Give it a try! 🥷⚒️💥