2021

Tiny Pixels CLI

A CLI to handle resizing and compressing bulk images.

NodeJS
TypeScript
sharp
npm

What is this?

Simply put, this is a tool to run over a bunch of images and produce multiple variations of each image. It is configurable in a way that lets you generate different sizes, qualities and image formats. It also generates a JSON file containing an array of all images and the different variants it has generated. This can later be used to display a large amount of images at once.

Why?

For a project that is near and dear to my heart, I had to handle a lot of photos. And I mean ALOT. There were about 2,000 photos in total. I wanted to display them in a square tile gallery layout mixed in with some text, but also wanted to be able to open them in a model and show a high quality version of them.

The problem

The source images were from all kinds of different cameras and phones, so there was no consistency when it came to quality and compression. It was clear that I needed multiple versions of the same image, and ideally have them all in the same size and quality. I needed one smaller one for the gallery layout (square) and one bigger one for the model view, which would keep the photo's original aspect ratio. I also wanted them in a couple of different sizes each to make sure they were all looking great even on higher pixel density screens.

Another concern was the amount of data that was going to be loaded, once the entire page was rendered on screen. If I had to load all the images in their original resolution at once, even with a reasonably high bandwidth, the time it would take to load all of that data would be seriously annoying and downright criminal when used on a mobile device.

Also, there was no way a browser could be able to handle that much data without seriously compromising the user experience.

The solution

Luckily, the modern web could help me out here. Nowadays, you can simply pass loading="lazy" to an img tag and the browser would only load the image once a user comes close to scrolling the image into view - no JavaScript required. It's basically magic.

Another marvel of modern computer science is image compression. JPEG is a fantastic format, and its ubiquitous support across all platforms and devices makes it a great format for the web. Unfortunately, it is starting to show its age and other formats are able to offer much higher compression rates. WebP is already supported by all major browsers, so we can show great looking images at a fraction of the costs of old JPEG images. Even more advanced is the AVIF format, which uses the AV1 compression algorithms to archive even greater compressions rated than WebP. It's just starting to arrive in browsers, but thanks to the source tag we can simply provide all these formats and let the browser choose automatically which format it is able to handle. Again, no JavaScript required.

Combining all of these techniques, we are able to keep the amount of image data required to render a website under control, while still providing a great visual user experience.

The Tech

I decided to use NodeJS and write the CLI in TypeScript, because it is very fast to start writing code without having to set up everything.

For dealing with resizing and converting the source images to different image formats, I went with the sharp library. I had used sharp in the past and found it to be wonderfully easy to work with. It also recently added support for AVIF, which is why I wanted to build this whole CLI in the first place.

To handle inputs I used yargs and to sprinkle in some nice visual feedback I added ora.

In the end, the logic it uses is quite simple:

  1. Run over all images in a given folder
  2. Generate different images from it based on the configuration
  3. Output the results in a JSON file (optional)

It will generate a couple of different default sizes, which can be configured by using the -s option. Each configuration for a new image looks like this - [size][min|max|squre]@[quality]. So if you want a square image that is 400x400 pixel with a quality of 60 this is the string to make that happen - 400square@60.

It is also possible to generate images in different image formats by changing the -f option. Supported output formats are JPEG, WebP and AVIF. Since AVIF encoders are just starting to get better (faster) and hardware support is not guarantied on every machine yet, the conversion process can be a lot longer than expected. I observed the average conversion time for a 12MP image to be around 30sec for AVIF, whereas JPEG and WebP took less than a second. For my purposes it was quite alright to wait a couple of hours to let the whole process finish, since I was looking for performance gains when it comes to delivering these bytes to the end user.

I tried to normalise the quality setting for JPEG, WebP and AVIF, since they seemed to produce different visual qualities when comparing results of the same quality setting. This is purely based on my eye test and does not use any scientific process whatsoever.

Results

To get some more solid test results, I used a subset of the splash open library dataset. It's actually meant for training and testing AI models, but it's also a great library if you are looking for a diverse set of images. I downloaded 500 randomly and ran my CLI over them with the following configuration.

Looking at how much data could actually be saved just by using a different image format is quite staggering. We have an average reduction of around 88% just by using AVIF over the original JPEG. But if you want to stick with JPEG (and there are some good reasons to stick with it as I mentioned) using the improved JPEG encoder by Mozilla still nets you a nice 71% reduction in size. That's without even reducing the size or quality of the image.

JPEGMOZ JPEGWebPAVIF0 MB1 MB2 MB3 MB4 MB5 MB6 MB7 MB8 MB
Image sizes by image format

If you want to reduce the amount of data loaded even further, we can look at serving the image at a reasonable size. If we look at the original problem a bit closer, we notice that I want to display a lot of images that are fairly small. For this case, you would want to reduce the size of the image as much as you can. The original resolution of the images in the splash library varied quite a bit, some were below 1MP, others were 34MP, but most of them were in the 12MP to 24MP range. As you can see below, the average size for the original images is just over 8.5 MB. The reduced size to a square of 256 pixel doesn't even appear, because it's a 99.91% decrease in data with an average size of about 7.6 KB.

Original2560640256sq0 MB1 MB2 MB3 MB4 MB5 MB6 MB7 MB8 MB
Image sizes by resolution

Future

Besides keeping the sharp version up and getting further performance improvements, I would like to add the ability to generate tiny preview images to the CLI. The idea is that these images would be extremely small, about 16 pixels, and they would be encoded in Base64 to include into the frontend bundle. This way, the images could be displayed immediately and act as a placeholder until the actual image has been loaded. Thanks to modern CSS filters, we can also make them visually more appealing by adding a blur filter to it so that the image appears blurry but not pixelated as it otherwise would be.

Future Future

It would be cool to provide the full suite of tools to deal with generated images. A package would provide React components à la Next.js to interpret the generated JSON file. The package would include a component to display the correct image. The component could deal with serving the right image format based on the browsers support, as well as provide properties to select which size to display. To improve the developer experience, the CLI could then generate a type declaration file to support full type safety. These types would help define which props can be passed to the image component. Also, a small set of utility functions to help sort and order the images would be helpful since the JSON file includes a bunch of meta information taken from the image Exif data.