Fork me on GitHub

Images

by Lucas Calloch via unsplash

Images add a critical element to any website - as a means of communication or simply to set the scene. Finding the right image for your project can be challenging, but serving it should not be. Although the native <img> element is powerful, it doesn't take full advantage of modern advances in image distribution.

For one, there are many better image formats than JPEG - such as WebP. They are smaller and more versatile, and are supported by many modern browsers. Additionally, with devices ranging from tiny androids to the largest desktop screens, there is no one size fits all resolution to provide. Choose too small a resolution and it's blurry on high-definition devices. Choose too large a resolution and mobile users might back out before the image even loads.

Fortunately, Vanilla Plus JS provides a simple preprocessing command to take any image, convert it to the appropriate formats and resolutions, and bundle them into a picture element. This means the browser will choose the best format and resolution for the device, with no additional configuration required.

To get started, simply add the desired image within your src/public/img/ folder, and then add the following code to your HTML file to generate the picture element:

<!--[IMAGE: /img/example.jpg 300 400]-->

This will generate a picture element whose source comes from the post-processed representations of the image, cropped to the desired aspect ratio (in this case, 300x400), and scaled to the appropriate device resolution. If you're curious how this picture element looks or how to style it, just inspect this page or check out the source code.

This accepts more arguments, though the defaults are usually sufficient. The format is:

path width height [cover] [crop_arguments] [lazy OR eager]

Where cover, lazy, and eager are literal strings. The crop arguments are specified as a json object. The object must be a subset of the following, where the values in the example are the default values if not specified:

{
    "crop_left_percentage": 0.5,
    "crop_top_percentage": 0.5,
    "pre_top_crop": 0,
    "pre_left_crop": 0,
    "pre_right_crop": 0,
    "pre_bottom_crop": 0
}

Where crop_left_percentage dictates the proportion to crop from the left when we need to crop the image to be thinner to match the aspect ratio. The default value of 0.5 means crop evenly from the left and the right, a value of 0 would mean crop exclusively from the right. Similarly, crop_top_percentage dictates the proportion to crop from the top when we need to crop the image to be shorter to match the aspect ratio.

Further, pre_top_crop simply crops the given number of pixels from the top of the image before applying the standard cover fit algorithm. pre_left_crop, pre_right_crop, and pre_bottom_crop similarly crop the given number of pixels from the left, right, and bottom of the image respectively.

Finally, lazy and eager are used to specify whether the image should be loaded asynchronously or not. By default, images are loaded asynchronously on supporting browsers via the lazy property, but you may specify eager to load the image before continuing to the next element.

That covers how to use the image preprocessing. For a bit more detail in how it works, it will sample various compression levels and resolutions on the first pass. It will prefer lower compression levels and also lower file sizes. These two things are in conflict, so the calculation is managed by assigning a "preference" to each compression level in vanillaplusjs.json, and then to compare two compression levels we divide the file size by the preference to get a penalty for each, then select the compression level with the lowest penalty. We repeat this for each resolution.

To avoid edge-cases where very small files are compressed exceptionally well, a minimum unit size is specified, where there is no decreased penalty below that file size. Furthermore, the resolution we are attempting will rule out certain compression levels, and hence the build time is dramatically reduced by not attempting those. For example, it will never make sense to compress a 300x300 image to 50% quality, or to use a lossless format for a 3000x3000 image. And generating that lossless format to test its size can take upwards of a minute. This is what the min_area_px2 and max_area_px2 parameters describe.

Processing is done with Pillow, so you can use any image format or compression level supported by that library in the configuration.

JavaScript

Although producing the picture element covers most use-cases for images, it's sometimes convenient to use the preprocessor to generate images which are then converted to HTML code in JavaScript. This is typically for more dynamic pages, where the page layout is generated on the fly.

It would be possible, although painful, to generate hidden picture elements and then parse the element to determine where the image is located. However, Vanilla Plus JS provides a direct way to accomplish this via .images.json files, which are handled specially by the preprocessor.

Suppose you had a highly dynamic profile page which required a default avatar image which you want to display via JavaScript after determining the user does not have a profile picture set. You could create the following file at src/public/js/profile.images.json:

{
    "default_avatar": {
        "path": "/img/default_avatar.jpg",
        "width": 20,
        "height": 20,
        "crop_style": "cover",
        "crop_arguments": {},
        "lazy": true
    }
}

Then the preprocessor will generate a file at src/public/js/profile.images.js with content like the following:

export default {
    "default_avatar": {
        "target": {
            "outputs": {
                "jpeg": [
                    {
                        "choice": "100",
                        "height": 20,
                        "url": "/img/default_avatar/1/20x20.jpeg",
                        "width": 20
                    },
                    {
                        "choice": "100",
                        "height": 30,
                        "url": "/img/default_avatar/1/30x30.jpeg",
                        "width": 30
                    }
                ],
                "webp": [
                    {
                        "choice": "lossless",
                        "height": 20,
                        "url": "/img/default_avatar/1/20x20.webp",
                        "width": 20
                    },
                    {
                        "choice": "lossless",
                        "height": 30,
                        "url": "/img/default_avatar/1/30x30.webp",
                        "width": 30
                    }
                ]
            },
            "settings": {
                "crop": "cover",
                "crop_settings": {
                    "crop_left_percentage": 0.5,
                    "crop_top_percentage": 0.5,
                    "pre_bottom_crop": 0,
                    "pre_left_crop": 0,
                    "pre_right_crop": 0,
                    "pre_top_crop": 0
                },
                "height": 20,
                "width": 20
            }
        }
    }
};

Which can be imported into JavaScript as follows:

import images from '/js/profile.images.js';

Furthermore, it will generate a placeholder file at src/public/js/profile.images.js with the appropriate typehints to ensure that it nicely integrates with your IDE.