blog

Responsive Images with Jekyll and ImageMagick

Step by step through the HTML, ImageMagick and Ruby. Works with Jekyll 4.

Lawrence Murray 30 October 2022

The Jekyll plugin described here is now available, if you just want to get it installed.

A responsive design adapts to display size to improve the user experience. An example is a grid layout that increases the number of columns with the width of the screen, from one on a narrow mobile device, to several on a wide screen display. A responsive design is made up of responsive design elements, among them responsive images, which help to:

  1. deliver appropriately sized and compressed variants of images to minimize transfer time and optimize performance,
  2. deliver appropriate content at those various sizes, e.g. using simpler artwork for smaller icons, rather than merely shrinking larger artwork, to preserve clarity.

Here, we address the first motivation, showing how to automate image resizing for Jekyll sites, by using ImageMagick command-line tools.

While the second motivation makes sense for all image formats, the former only makes sense for raster formats such as PNG and JPEG. Vector formats such as SVG are already responsive, in that they scale up and down without loss in clarity or change in file size.

HTML essentials

Responsive images are enabled by two attributes of the HTML img element:

An img element should also provide the common src, alt, width and height attributes. Putting them all together, it should look something like this:

<img
    src="example.jpg"
    alt="An example image"
    srcset="example-w256.jpg 256w, example-w512.jpg 512w, example.jpg 1024w"
    sizes="(max-width: 575px) 100vw, (max-width: 767px) 50vw, (max-width: 991px) 34vw, (min-width: 992px) 25vw"
    width="1024"
    height="768"
>

Here, the srcset attribute provides three image variants:

  1. example-w256.jpg of width 256 pixels,
  2. example-w512.jpg of width 512 pixels, and
  3. the original example.jpg of width 1024 pixels.

The sizes attribute provides guidance as to the context in which the image appears:

  1. if the viewport width is up to 575 pixels (575px) then there is one column (100vw i.e. 100% of viewport width),
  2. if the viewport width is up to 767 pixels (767px) then there are two columns (50vw i.e. 50% of viewport width),
  3. if the viewport width is up to 991 pixels (991px) then there are three columns (34vw—rounding up),
  4. otherwise there are four columns (25vw).

This suggests that the image will appear as part of a one to four column layout, depending on display width.

Automating srcset is achievable; we do so here. Automating sizes is much more difficult as its value depends on the context in which the image appears; we do not attempt to do so here.

The particular breakpoints chosen above are derived from those in Bootstrap.

ImageMagick essentials

ImageMagick is a command-line tool that can query and modify images in a wide variety of formats. We use its convert tool for resizing images:

convert example.jpg -strip -quality 30 -resize 256 example-256w.jpg

Here, we convert example.jpg to example-256w.jpg, stripping meta data (-strip) setting the quality (JPEG compression in this case) to 30, and resizing to a width of 256 pixels (the height will be scaled proportionally).

We also use ImageMagick’s identify tool to obtain the widths and heights of images:

identify -ping -format '%w,%h' example.jpg

The -ping option avoids consuming the entire file to obtain this information. The -format option gives the output format, in this case the width and height separated by a comma ('%w,%h'). The output will be e.g. 1024,768.

ImageMagick has numerous libraries available for various programming languages. However, we will use the command-line interface directly to minimize dependencies, given that our needs are simple.

Jekyll plugin

We now develop a Jekyll plugin that automates image resizing for a given set of widths. For convenience, it also provides filters to set srcset, width and height attributes.

Configuration

Add the following to the site’s _config.yml:

responsive:
  widths: [400,500,700,900]
  quality: 30

This configures the widths of resized images and their quality. The choice of widths should cover the typical sizes of images as they appear on the site. The choice of quality (between 0 and 100) is a trade-off between file size (lower at lower quality) and clarity (higher at higher quality). It could be tuned by eye. Low quality can be very satisfactory with the prevalence of high definition displays.

For indii.org, where most images are photos, I find noticeable degradation in thumbnail quality at 20, but little at 30. That thumbnails link to original high-resolution images also permits the quality to be reduced. For widths values I iterated with PageSpeed Insights to assess performance, and settled on the above configuration.

Installation

The plugin requires ImageMagick. It is standard in Linux distributions and available through Homebrew on macOS. If not already, it can be installed with e.g.:

OS Command
Ubuntu apt-get install imagemagick
Fedora dnf install ImageMagick
openSUSE zypper install ImageMagick
macOS brew install imagemagick

If you happen to be hosting with CloudFlare Pages, it is already installed.

Copy the Ruby code below to _plugins/jekyll-responsive-magick.rb:

require 'fileutils'

module Jekyll
  module ResponsiveFilter
    @@sizes = {}

    def identify(input)
      site = @context.registers[:site]
      if site.config['responsive']['verbose']
        verbose = site.config['responsive']['verbose']
      else
        verbose = false
      end
      cmd = "identify -ping -format '%w,%h' .#{input.shellescape}"
      if verbose
        print("#{cmd}\n")
      end
      @@sizes[input] = `#{cmd}`.split(',', 2).map!(&:to_i)
    end
  
    def srcset(input)
      site = @context.registers[:site]
      if not input.is_a? String || input.length == 0 || input.chr != '/'
        throw "srcset: input must be absolute path"
      end
      dirname = File.dirname(input)
      basename = File.basename(input, '.*')
      extname = File.extname(input)
      src = ".#{dirname}/#{basename}#{extname}"
      srcwidth = width(input)      
      srcset = ["#{input} #{srcwidth}w"]

      if File.exist?(src) and ['.jpg', '.jpeg', '.png', '.gif'].include?(extname)
        dest = site.dest
        if site.config['responsive']['widths']
          widths = site.config['responsive']['widths']
        else
          # as default, use breakpoints of Bootstrap 5
          widths = [576,768,992,1200,1400]
        end
        if site.config['responsive']['quality']
          quality = site.config['responsive']['quality']
        else
          quality = 80
        end
        if site.config['responsive']['verbose']
          verbose = site.config['responsive']['verbose']
        else
          verbose = false
        end
        
        widths.map do |width|
          if srcwidth > width
            file = "#{basename}-#{width}w#{extname}"
            dst = "_responsive#{dirname}/#{file}"
            if not site.static_files.find{|file| file.path == dst}
              site.static_files << StaticFile.new(site, "_responsive", dirname, file)
              if not File.exist?(dst) or File.mtime(src) > File.mtime(dst)
                FileUtils.mkdir_p(File.dirname(dst))
                cmd = "convert #{src.shellescape} -strip -quality #{quality} -resize #{width} #{dst.shellescape}"
                if verbose
                  print("#{cmd}\n")
                end
                system(cmd)
              end
            end
            srcset.push("#{dirname}/#{file} #{width}w")
          end
        end
      end
      return srcset.join(', ')
    end

    def width(input)
      if not input.is_a? String || input.length == 0 || input.chr != '/'
        throw "width: input must be absolute path"
      end
      if not @@sizes[input]
        identify(input)
      end
      return @@sizes[input][0]
    end

    def height(input)
      if not input.is_a? String || input.length == 0 || input.chr != '/'
        throw "height: input must be absolute path"
      end
      if not @@sizes[input]
        identify(input)
      end
      return @@sizes[input][1]
    end
  end
  
end

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

Usage

The plugin provides three filters: srcset, width and height. Each consumes an absolute path to an image (as it would appear in src) and generates a value for the corresponding attribute. The intended usage is:

<img
    src="{{ src }}"
    srcset="{{ src | srcset }}"
    width="{{ src | width }}"
    height="{{ src | height }}"
>

src must be an absolute path, i.e. beginning with /, such as /assets/example.jpg. This is necessary for the plugin to find the right file in your project.

With the plugin installed and one or more uses of the srcset, width or height filters, you can build your site as normal, now with responsive images.

Testing

To test that responsive images are working, try:

  1. Inspecting the rendered HTML of pages in the _site directory, or via your browser by right-clicking and selecting “View source”. Verify that the srcset, width and height attributes are set correctly.
  2. In your browser, right clicking an image and selecting “Open image in new tab” (works with Firefox, Chromium and Chrome). Verify that the expected image variant has been used by inspecting the URL of the new tab. It should have a name such as example-w256.jpg, where 256 denotes the resized width.
  3. Checking your page with PageSpeed Insights to verify that appropriate image variants are being selected for different devices.

Further details

The srcset, width and height filters call ImageMagick’s convert and identity on demand, rather than for all image assets in a project. Caching is used to call identity at most once per image per build, and convert once per image per width, storing resized images in a subdirectory _responsive/ for reuse in subsequent builds. Resized images in _responsive/ are updated only if their original source image changes (detected using last modified times on files).

If you experience any issues with outdated images, or simply wish to clean up, remove the whole _responsive/ directory and rebuild. You may also wish to do this if you change the widths option in _config.yml.

When using width and height, expect additional build time of about one second per 100 images due to the overhead of launching identity processes.

When using srcset for the first time, expect additional build time on the order of minutes as resized images are generated with convert. Performance will improve drastically on subsequent builds because of the _responsive/ subdirectory.

Summary

With a combination of a plugin and ImageMagick, we can automate the addition of the srcset attribute to img elements for Jekyll sites, creating responsive images to minimize transfer time and optimize website performance.

blog Latest
GPU Programming in the Cloud
How to develop on remote cloud instances, and a roundup of cloud service providers.

Lawrence Murray

22 Nov 22

GPU Programming in the Cloud
blog Related
Matrix Gradients of Scalar Functions
Understanding the building blocks of reverse-mode automatic differentiation.

Lawrence Murray

7 Nov 22

blog Next
Admonitions in Markdown
Working or failing gracefully across Apostrophe, Kramdown, and Jekyll. No plugins required.

Lawrence Murray

2 Nov 22

research Previous
Probabilistic programming: a powerful new approach to statistical phylogenetics

F. Ronquist, J. Kudlicka, V. Senderov, J. Borgström, N. Lartillot, D. Lundén, L.M. Murray, T.B. Schön, D. Broman