Lazy image resizing in hexo-image-sizes v2

23 April, 2018

I just released version 2 of hexo-image-sizes, which represents a complete rewrite of the plugin. It finally supports lazy image resizing. In this post, I will cover the high-level design ideas behind the new version and some of the challenges in making it.

Check out my first post on it for more background on the functionality.

Lazy image resizing with imsize tags

hexo-image-sizes uses a special tag format for adding images to posts in Hexo. Users add an imsize tag to their post when they want to use an image with the plugin, and in the tag they can specify various details of the image including its size profile, its alt text, its link target, and more.

Here’s an example use of imsize within a markdown post:

{% imsize %}
src: /uploads/2010/06/ArduinoDuemilanove.jpeg
alt: Arduino Duemilanove
profile: body
{% endimsize %}

When Hexo renders a post containing an imsize tag, it invokes the tag’s registered function. The tag function has access to the user’s arguments and some context information including the current page on which it appears, which will be important for resolving filenames.

The imsize tag affords the plugin a special ability. Because users must invoke the tag when they want to embed a resized image in their post, hexo-image-sizes can monitor exactly which images the user has actually made visible in his or her posts. The new version of the plugin exploits this fact to avoid resizing images that the user never makes visible on the site.

The plugin’s operation now comprises two distinct phases during static site generation:

  1. First, while Hexo renders each post on the site, the plugin monitors all the imsize tags the user includes in posts. Each time the user includes an imsize tag, the plugin records which image the user included and the desired new size in a cache in memory.
  2. After Hexo has processed all of the posts in the site, the plugin knows which images the user wants to make visible. It generates resized versions of each visible image.

Using Hexo’s router to avoid filesystem manipulation

Hexo has a router module responsible for tracking which files are in use on the site and what their contents should be. The router is a map from the path of the file (relative to the Hexo site’s public output directory) to a stream of the contents of the file.

The documentation for the router is missing some important details. First, it’s very important to know that each route corresponds to a a file in the public directory and not in the source directory. Therefore, if you add a route, you will add a file to the public site but not alter the source directory. When I first began work on this plugin, I assumed that the routes referenced files in the source directory. I learned about using the router from a similar plugin, hexo-filter-responsive-images.

Using the router to add new resized images to the public output of the site generator has several advantages. By getting each file’s contents from the route stream managed by the router, my plugin can start to cooperate with other asset-management plugins, like image filters and minifiers. Using the router also allows me to remove the hacky filesystem code I was using before, where I had to guess the output location of each file and create the directory structure there.

Using the router also has a downside. I’m using sharp to resize the images, and it takes as input either a file or a Buffer of image data, while the router provides a stream for each file. To run the image through sharp, I have to read the full file stream into memory (in a Buffer), which adds a lot to the memory use of the plugin. In its original form, the plugin would invoke sharp with file pointers instead of file contents, which meant that only sharp would need to see the full contents of the image, outside of the JavaScript VM. For my site, the increased memory use has not been an issue.

Keeping Hexo running until all images are resized

I discovered that running Hexo in server mode with hexo server can behave differently than running it to generate files and quit with hexo generate. My site would generate without issue when running the server, but hexo generate would only produce a few of the necessary images.

The problem was with my after_generate filter function. Hexo uses the return value of filters to determine when they are finished running, and my filter wasn’t returning anything. When Hexo was running in server mode, it would keep running indefinitely, long enough to allow my plugin to finish resizing everything. In generate mode, it would exit as my plugin was just starting, because my filter function seemed to run synchronously.

To fix the issue, I simply return a Promise from the filter function. The Promise resolves when every image has been resized.

Normalizing file paths throughout the application

Hexo uses several different types of file path, and mixing them was hindering my development of the plugin for a while. The following paths are involved in resizing an image:

  • The path the user put in the imsize tag, which could be:
    • Absolute (with a leading slash), meaning it references the Hexo source directory
    • Relative (no leading slash), meaning it is relative to the current post:
      • If the current post is a blog post and has an asset directory (post_asset_folder) is true in Hexo config, then the path starts in the asset directory
      • If the current post is not a blog post or post_asset_folder is false, then the path starts in the directory of the post file.

The plugin also has access to:

  • The absolute path to Hexo’s source directory
  • The absolute path to the current post’s source file
  • The absolute path to the current post’s asset directory, if there is one

The plugin computes:

  • A relative path from Hexo’s source directory to the image the user wants to resize. For example, a cat picture might have path /posts/2018-cats/cat1.jpg
  • A copy of that relative path with the image renamed to have its profile name as a prefix. For example, a thumbnail-sized version of the above cat picture might have path /posts/2018-cats/thumbnail-cat1.jpg

Once we know the Hexo-relative path to the input image and the output image, we have all the information necessary to resize the image.