Skip to title

Image Resizing

Page updated

My journey building simple plugins to modify and add to the Jekyll build process.

Motivation and context

Jekyll icon What is Jekyll?

To keep it brief Jekyll is an SSG (Static Site Generator). It’s used by Github for Github pages and it’s what was used to build the site you’re seeing right now!

This post will assume you have used Jekyll.

The problem(s)

As I have been creating this blog I’ve found a desire to extend and modify the Jekyll build process.

Additionally, assets (such as images) are not located relative to individual blog posts but are lumped together at the top of assets/.

The result is either

  • living with all assets at top level and risking assets with the same name overwriting one another
  • creating a system of subfolders which need to referenced for each blog post

After attempting to reference an image for the fourth time via

1
![](assets/images/2021-06-28-image-resizing/dog.jpg)

then trying

1
2
{% assign img_path = "assets/images/2021-06-28-image-resizing/" %}
![]({{ "dog.jpg" | prepend img_path }})

I decided something more complex is necessary.

Solutions

A naive solution would be to create a script that ran the desired programs after Jekyll had generated the necessary files. It would look something like this,

1
2
3
4
5
6
7
8
9
#/usr/bin/env sh

# ... preprocessing steps
optmise_media assets/media/*.*

bundle exec jekyll build

# ... postprocessing steps
minify assets/js/*.*

Unfortunately this script would be losing out on the live reload feature jekyll serve provides. Performing manual rebuilds is not the end the world but it is a pain point.

What is already available

There was a Jekyll plugin but it was quite old. It makes use of Imagemagick to resize images to whatever size is defined as a filter parameter.

1
2
It uses a filter like so
{{ "path/to/assets/image.png" | resize: "800x800>" }}

Using Imagemagick has the additional benefit You can use whatever you want as the second parameter as long as Imagemagick can read it.

My solution

Image optimisation

image_optim is a Ruby library that contains a variety of optimisation utilities.

Running this library across all the images after they have all been generated could be an easy first step.

Since I’m not creating a gem for this first plugin I have to create my script under the _plugins/ folder which needs to be created. Based on the Jekyll plugin installation documentation.

In order to import image_optim I appended this package to the bottom of my Jekyll project’s Gemfile.

1
2
3
4
5
6
7
8
9
# ... the other stuff

# For syntax highlighting
gem 'rouge'

# For /_plugins/
gem 'image_optim'
gem 'image_optim_pack'
gem 'rmagick'

The Jekyll Hooks makes it very easy to run code at the right point in the build process. In this case I perform a pass on all the images and optimise them in place.

1
2
3
4
5
6
require 'image_optim'

Jekyll::Hooks.register :site, :post_write do
  image_optim = ImageOptim.new
  image_optim.optimize_images!(Dir['_site/assets/images/*/*.*'])
end

The best part, it reruns whenever the site live reloads!

The worrying part, it reruns whenever the site live reloads! If the number of images increases a lag spike could become apparent whenever we build our site this would be especially pronounced for local development. From my understanding, Jekyll will copy the unoptimised images from the top level /assets/ folder to the /_site/ folder each reload therefore it isn’t possible to improve the performance of this plugin without turning it off completely.

¯\_(ツ)_/¯ given the amount of extra effort needed and the minor reward to solve this I think I can throw this on the backlog for future Josh.

We’re going to use this handsome fellow from pxfuel.com for comparisons.

dog

1
2
3
4
5
6
7
# original image
du -h assets/images/2021-06-28-image-resizing/dog.jpg
# 236K

# image optimised
du -h _site/assets/images/2021-06-28-image-resizing/dog.jpg
# 148K

This outcome was obtained after tweaking some of the image_optim settings to run lossy jpeg compression with the lowest quality option. The image above is the 148K version and it looks perfectly fine.

As you saw, the above is the entire plugin for adding a new, simple step to the build process. Huge props to Jekyll for making changes like this is as simple as writing a script.

Image resizing

The next stage was to control the size of images. I could have copied the jekyll-resize gem and called it a day but there were two aspects of the old plugin I wanted to improve.

The first aspect was discussed in The problem(s). Having to write out links to grab the image you want gets in the way of writing and disincentives adding visuals to blog posts.

The second was how granular image resizing was. As I write posts I don’t want to think about exact image sizes and how they will fit with page composition, I want quick defaults and further tweaking later on.

The solution I propose

1
2
It uses a filter like so
{{ "image.png" | resize: "large" }}

In order to resize the images I used rmagick which had decent documentation. Resizing was an easy process

Expand for embed_asset_v1.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
require 'mini_magick'

module Jekyll
  ##
  # contains the embed_image filter
  # Usage: {{ "path/to/image" | embed_image: "large" }}
module MediaManagementFilters
    def embed_image(filepath, size = nil)
      return filepath if size.nil?

      img = MiniMagick::Image.open(".#{filepath}")
      limit = !named_sizes.nil? && named_sizes.key?(size) ? named_sizes[size] : Integer(size)

      if img.width <= limit
        puts "Original image #{filepath} smaller than the #{size} specification"
        return filepath
      end

      dir = "#{File.dirname(filepath)}/"
      filename = File.basename(filepath)
      resized_filename = "gen-#{limit}-#{filename}"
      resized_filepath = "#{dir}#{resized_filename}"

      # This part derived from jekyll-resize
      if !File.exist?(".#{resized_filepath}") || File.mtime(".#{resized_filepath}") <= File.mtime(".#{filepath}")
        img.resize(limit.to_s)
        img.write(resized_filepath)

        site = @context.registers[:site]
        # Jekyll has already scanned for static files (to copy to _site later)
        # Need to add these static resources manually
        site.static_files << Jekyll::StaticFile.new(site, site.source, dir, filename)
      end

      resized_filepath
    end
  end
end

Liquid::Template.register_filter(Jekyll::MediaManagementFilters)

The code above matches the feature set of the old jekyll-resize plugin. But now allows for some words to map to values. In my _config.yml I have

1
2
3
4
5
6
7
8
# stuff...

image_sizes:
  large: 900
  medium: 450
  small: 200

# other stuff...

The site.static_files << Jekyll::StaticFile.new(site, site.source, dir, filename) on line 31 of embed_asset_v1.rb was there to let Jekyll know of new static resources. From my understanding a scan of the available static resources is performed early on. The resources generated via this liquid filter have to be manually added.

Image location

Looking good so far but I still have to specify a verbose path to find the desired image.

1
{{ "path/to/assets/image.png" | embed_image: "large" }}

The technical problem, the filter did not let me know what page I was compiling; not in any obvious way.

After searching online and printing the contents of a bunch variables Jekyll provided I noticed that the @context.registers contained more than just the site-wide context. @context.registers[:page] which is of class Drop looked like what I was looking for.

Attempting to print this object resulted in a recursion limit error which was a huge pain. My current hypothesis, the object attempts to evaluate and print each of it’s variables which might start recursively printing. This would not be a problem if the site structure was like a tree without loops, unfortunately this is not the case. This comment in the Jekyll repo indicates the same. There was probably a better way to print the objects but I ended up printing the keys (which did not cause a recursion issue) and accessing the underlying value.

'slug' (the url friendly version of the title but essentially the title if the original is url friendly) contained the named portion of the filename and 'date' was the value prepended to produce %Y-%m-%d-{slug} which is the filename of the post. After a little bit of refactoring I ended up with the following,

Expand for embed_asset_v2.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# frozen_string_literal: true

require 'mini_magick'

module Jekyll
  ##
  # contains the embed_image filter
  module MediaManagementFilters
    ##
    # the filter embed_media prepends /assets/media/Y-m-d-page-name/ to the input
    # provide a page to make the path relative to that page instead
    def embed_media(filename, page = nil)
      "#{media_dir page}#{filename}"
    end

    ##
    # performs embed_asset on the input then generates resized versions of the file
    def embed_image(filename, size = nil, page = nil)
      dir = media_dir page
      return "#{dir}#{filename}" if size.nil?

      img = MiniMagick::Image.open("./#{dir}#{filename}")

      site = @context.registers[:site]
      limit = image_size(size, site.config['image_sizes'])
      return "#{dir}#{filename}" if img.width <= limit

      resized_filename = save_image(img.resize(limit.to_s),
                                    site, dir, "gen-#{limit}-#{filename}",
                                    File.mtime("./#{dir}#{filename}"))
      "#{dir}#{resized_filename}"
    end

    private

    def media_dir(page = nil)
      page ||= @context.registers[:page]
      page_name = page.key?('date') ? "#{page['date'].strftime('%Y-%m-%d')}-#{page['slug']}" : page['slug']
      "/assets/media/#{page_name}/" # path between site.source and the file
    end

    def image_size(size, named_sizes = nil)
      # this is the width limit
      !named_sizes.nil? && named_sizes.key?(size) ? named_sizes[size] : Integer(size)
    end

    ##
    # If the image is older than mtime then it gets rewritten
    def save_image(img, site, dir, filename, mtime = Time.new(0))
      fullpath = "#{site.source}#{dir}#{filename}"
      if !File.exist?(fullpath) || File.mtime(fullpath) <= mtime
        img.write("#{site.source}#{dir}#{filename}")
        # Jekyll has already scanned for static files (to copy to _site later)
        # Need to add these static resources manually
        site.static_files << Jekyll::StaticFile.new(site, site.source, dir, filename)
      end

      filename
    end
  end
end

Liquid::Template.register_filter(Jekyll::MediaManagementFilters)

The filter syntax now has the additional property page. You may want to reference an image from another page. Since the post objects used in site.posts are of the same type as the one returned by @context.registers[:page] we can specify an explicit page to make our path relative to.

I added embed_media which only performed the path expansion portion. Items like SVGs can benefit from the filter without rmagick attempting to shrink it.

Conclusion

Prior to this blog post I had no idea about any Ruby syntax or what made Ruby tick. Learning about the syntax and the way objects and classes work has been very interesting. rubylearning.com was a great resource to get up to speed quickly.

The plugins written remain in my _plugins/ folder for now. The current approach is very opinionated on how assets should be organised and I suspect they scripts won’t be very helpful as installable gems.

For now I encourage people who would like to use these scripts to create a _plugins folder and modify them yourself. If there is some interest I’ll look into packaging them into gems.

This is how I am currently using my new filter in this website.

1
2
3
4
On the front page
<img src="{{ post.image | embed_image: "small", post }}" alt=""/>
or in this post
![Small picture of a dog]({{ "dog.jpg" | embed_image: "small" }})

Small picture of a dog