Optimize image thumbnails

The default thumbnailer for Romanesco is pThumb. This extra utilizes the phpThumbOf library, which is embedded in MODX. It is flexible and reliable and has been working well for many years, but now it's starting to show its age a little. Modern image formats such as WebP are not supported and the compression algorithm is also not as effective as modern counterparts.

The result: thumbnails are either too large or too ugly, depending on the quality setting.

Another problem is that with the introduction of responsive images, it became a lot harder to optimize and compress images upfront. You probably need to generate different versions of the image for different screen sizes and pixel densities after you upload the image file, so tools like ImageOptim or a plugin to resize the image on upload don't really fit this workflow anymore. Unless you're willing to generate all the responsive versions upfront too, but... Yikes!

Online platforms like Cloudinary, Cloudflare and Amazon are now offering these services through an API, but call me an oldfashioned file hoarder; I just want to have everything stored and running on my own machine.

The solution: keep using pThumb for creating all the thumbnails, but optimize them after they've been generated.

How does it work?

It starts with the imgOptimizeThumb snippet. That's an output modifier for pThumb, which runs after the thumbnail is generated.

It uses the libvips (Vips) library to create a WebP version of the image and optimize the original. You need to install the libvips package on your server.

If the Scheduler extra is installed, the resize commands are run in the background, triggered by a cronjob. This means it takes a little while for all the images to be generated. Without Scheduler they're created when the page is requested, but the initial request will take a lot longer (the thumbnails are also being generated at this point).

To serve the WebP images in the browser, use Nginx to intercept the image request and redirect it to the WebP version. It will do so by setting a different header with the correct mime type, but only if the WebP image is available (and if the browser supports it). So you don't need to change the image paths in MODX or provide any fallbacks in HTML!

How to activate it?

Server

Install the libvips library:

sudo apt install --no-install-recommends libvips42

(The required PHP library is already installed through Romanesco Backyard, verion 1.0.0-beta14 and up).

Nginx

Add the following to your nginx.conf:

http {
  ...
  map $http_accept $webp_ext {
    default "";
    "~*webp" ".webp";
  }
  map $uri $file_ext {
    default "";
    "~(\.\w+)$" $1;
  }
}

And in your server config under sites-available:

server {
  ...
  # Load WebP image if available
  location ~* "^(?<path>.+)\.(png|jpeg|jpg|gif)$" {
    try_files $path$webp_ext $path$file_ext =404;
  }
}

If you also want to set a longer cache lifetime, then add the following before that location block:

server {
  ...
  # Load WebP image if available
  location ~* "^(?<path>\/assets\/cache.+)\.(png|jpeg|jpg|gif)$" {
    expires 1y;
    add_header "Cache-Control" "public";
    try_files $path$webp_ext $path$file_ext =404;
  }
  
  # Add cache header to static assets
  location ~* '\.(?:jpg|jpeg|gif|png|webp|ico|etc...)' {
    ...
  }
}

The regex is constrained to the assets/cache folder now, so it won't interfere with the other static assets.

Source of this nifty little trick.

MODX

Activate the 'Optimize image' setting under Configuration > Performance (that's in ClientConfig). This tells the imgOptimizeThumb snippet to execute the relevant Vips command after the thumbnail has been generated.

And although entirely optional, it pays off to install the Scheduler extra. The imgOptimizeThumb snippet will detect Scheduler if it's installed and automatically handle task creation and batch execution. This drastically shortens initial page loads and keeps server load in check.

Changing global image quality

Image quality is defined under Configuration > Performance. This setting (img_quality) is usually forwarded directly to pThumb for its quality parameter. But now that we're post-processing our image after thumbnailing, it's actually better to generate the thumb with a high quality setting (separate from the img_quality value). Image optimization generally works better with a good source file.

To work this out, there is a system setting named romanesco.img_quality, which by default contains a placeholder for the img_quality setting under Configuration. But if we change this to something like 90, it means that the source file will be compressed with that quality setting and the optimized thumbs will use the config setting.

The Vips library does an impressive job of compressing images without much loss in detail, even at very low quality settings. So don't be afraid to set it to Low (50) or Minimum (30). For most images the difference is hardly observable, but it shaves off a substantial amount of bytes from the file size (sometimes as much as 80%).

And don't worry about the high quality original thumbs taking up a lot of disk space either. Vips will also optimize this image in place. They serve as fallbacks if WebP is not supported, or the .webp file is not yet generated.

Changing background image quality

There is also a setting available inside Global Background resources, to override the global quality setting defined under Configuration. Useful for edge cases such as graphical patterns and line drawings, which need a high quality setting to prevent them from getting blurry. Or when quality is not such a big deal and you really want file size to be as low as possible.

Clearing image cache

The entire process described here is only triggered if there is no WebP version of the image available in the thumbnail cache. And keep in mind that the original JPG is also optimized (meaning: less suitable as source file).

So if you want to optimize your images again after changing the settings, you need to clear the thumbnail cache first.

In Romanesco, images are cached under assets/cache/img. After clearing the cache, each resource needs to be accessed again in order to regenerate and then optimize the thumbnails. You can hire a bot to do a little cache warmup for you. Otherwise, your next website visitors will be waiting and waiting for their optimized images to appear, which kind of defeats the whole purpose of this exercise!

Can I use it without Romanesco?

I didn't try that yet, but I don't see why not. Just copy the imgOptimizeThumb snippet, remove the part on top that fetches the Romanesco class and replace the imgQuality variable with something that works for you.