On-demand image resizing with Bridgetown

As I recently migrated this blog from Middleman to Bridgetown, I thought I could document one of my Bridgetown customizations: on-demand or build-time image resizing.

Notes

In this approach, the originals are stored in the frontend/ directory and should therefore be also accessible via the Bridgetown built-in asset_path helper or through esbuild imports.

The resized images are stored in src/images/resized so that they don’t need to be processed by esbuild

The resized images contain a digest of the original (CRC32 by default) ensuring that they get reprocessed if the original changes.

The CRC hash should be sufficient for detecting changes in the original file in a non-adversarial environment, and it is very fast to compute.

The CRC code can be easily replaced with a more robust, though slower, cryptographic hash function if desired

On the other hand, I’ve chosen not to add a hash of the generated image to the filename in order to avoid having to keep track of the originals and generated files in a manifest file.

I’ve chosen not to commit the generated files to Git, but you might make a different choice based on your needs. If you do push the resized images to version control, make sure to also clean up any versions that are no longer needed.

Prerequisites and assumptions

For this tutorial, I’m assuming the following:

Bridgetown v1.3

Ruby 3.3

ERB templates instead of Liquid

ERB is going to be the default in Bridgetown 2.0

I prefer ERB to Liquid in any case

It should be possible to adapt these steps for Liquid templates, but I haven’t explored those details

You’re somewhat familiar with Ruby and Bridgetown already

You should have libvips installed

macOS: brew install vips

Debian/Ubuntu: sudo apt install libvips

Steps

If you don’t have a Bridgetown site yet, create one:

bridgetown new --templates=erb image-resizing-demo
cd image-resizing-demo

Add the ImageProcessing gem to the Gemfile:

gem 'image_processing', '~> 1.12', '>= 1.12.2'

Add a test image to frontend/images:

mkdir -p frontend/images
# You can, of course, use any image you like as your test subject…
curl --output frontend/images/blue-marble.jpg \
https://www.randomerrata.com/_bridgetown/static/blue-marble-T6POMCUA.jpg

Create plugins/builders/asset_helpers.rb:

require "image_processing/vips"
require "zlib"

# If you want to use a cryptographic digest, you can uncomment following:
#
# require "digest/sha2"

class Builders::AssetHelpers < SiteBuilder
def build
helper :resized_image_path do |src, width: nil, height: nil|
basename = File.basename(src, ".*")
ext = File.extname(src)

# Find images from the frontend directory
frontend_path = Pathname.new(site.root_dir).join("frontend")

image_path = frontend_path.join(src)

# Calculate a digest based on the original image file
#
digest = Zlib.crc32(File.read(image_path)).to_s(16)
#
# It's also possible to use a cryptographically secure digest but
# calculating it will be slower and it's not necessarily necessary for
# this use case.
#
# To use SHA256, uncomment the following and remove the CRC digest line:
#
# digest = Digest::SHA256.file(image_path).hexdigest
#

# Put resized images inside the src/images/resized directory
static_images_dir = Pathname.new(site.root_dir).join("src", "images")
destination_dir = Pathname.new(static_images_dir).join("resized")

# Generate the resized filename based on the original filename, digest, and dimensions
resized_filename = "#{basename}-#{digest}-#{width}x#{height}#{ext}"
destination_path = destination_dir.join(resized_filename)

# Don't do any processing if the file already exists
if !destination_path.exist?
Bridgetown.logger.info "Resizing image: #{src} to #{width}×#{height}"

FileUtils.mkdir_p(destination_dir)
pipeline = ImageProcessing::Vips.source(image_path).resize_to_limit(width, height)
pipeline.call(destination: destination_path.to_s)
else
Bridgetown.logger.debug "Resized image exists: #{destination_path}"
end

# Calculate the relative path to the resized image in the src/images folder
resized_path = destination_path.relative_path_from(static_images_dir)

# Make sure that Bridgetown knows about the resized image
site.static_files << ::Bridgetown::StaticFile.new(
site,
site.source,
"images",
resized_path
)

# Return the path to the resized image
"/images/#{resized_path}"
end
end
end

Add the test image to src/index.md:

---
layout: default
---

# Welcome to your new Bridgetown website.

![Blue Marble](<%= resized_image_path("images/blue-marble.jpg", width: 1024) %>)

... the rest of the file continues

Ignore the generated images in Git by adding resized directory to .gitignore (optional):

# Ignore generated images
/src/images/resized

Start the Bridgetown server: bin/bridgetown start

You should see the resized image when you visit http://localhost:4000

Screenshot