Creating thumbnails for images on a website is key to performant and responsive page loads. This is easy to do for things like screenshots that tend to be all the same size; however it can be problematic for images with varying sizes, especially elongated ones. In this article we’ll discuss a technique to generate thumbnails that preserve a consistent aspect ratio, by padding them as needed.
Generating Thumbnails with ImageMagick
In “Resizing Images and Creating Thumbnails with ImageMagick“, I demonstrated a simple way to resize images using ImageMagick:
convert input.jpg -resize 175x109 output.jpg
This works, and it preserves the aspect ratio, but disregards the desired size (in this case 175×109). For instance, applying this to a 712×180 image produces a thumbnail of size 175×44, which is undesirable in a page where your other thumbnails will be 175×109.
An alternative is to force the output size by adding an exclamation mark to the size:
convert input.jpg -resize 175x109! output.jpg
This produces an image with the desired size, but throws the aspect ratio out of the window. Elongated images end up with thumbnails that look squished.
Fortunately, ImageMagick already has the ability to pad out thumbnails, and so it is easy to produce a thumbnail that respects both the desired output size and the aspect ratio, by having it pad out the extra space in the thumbnail:
convert input.jpg -thumbnail 175x109 -background cyan -gravity center -extent 175x109 output.jpg
Note that I’ve used cyan only to demonstrate the effect of the padding; in practice you will want to use a colour that blends with the rest of the image, in this case white.
The resulting thumbnail preserves the proportions of the features of the original image, while fitting nicely into the 175×109 dimensions that will be used by all other thumbnails on the page.
Mathematical Foundation
By the time I discovered how to generate padded thumbnails using ImageMagick, I had already worked out the mathematical calculations to do it myself. While this is now unnecessary, it’s an interesting exercise and can be useful if you ever need to do this kind of thing yourself programmatically without the help of ImageMagick.
So, imagine we have an elongated image and we need to generate a thumbnail that fits a specific size and is padded out to keep the original image’s aspect ratio, same as we did with ImageMagick. One way to do this is to pad the original image with extra space to achieve the desired aspect ratio of the thumbnail, and then just resize to the desired thumbnail dimensions. This works out slightly differently depending on whether the image is horizontally or vertically elongated.
Let’s start with the first case: say we have an image of original size 600×200, and we want to fit it into a thumbnail of size 175×109. The first thing we need to do is calculate the aspect ratio of the original image and that of the thumbnail.
The aspect ratio is calculated simply by dividing width by height. When the aspect ratio of the original image is larger than that of the thumbnail, as in this case, it means that the image is horizontally elongated and needs to be padded vertically. Conversely, if the original image’s aspect ratio were smaller than that of the thumbnail, we would be considering the second case, i.e. a vertically elongated image that needs to be padded horizontally.
Now, we need to figure out the dimensions of the padded image. We already know that our 600×200 image needs to be padded vertically, so the width remains the same at 600, but how do we calculate the new height (new_h)? As it turns out, the Law of Similar Triangles also applies to rectangles, and since we want to keep a constant aspect ratio, then it becomes just a matter of comparing ratios:
To double-check the result, calculate its aspect ratio again. Dividing 600 by 373.71 does in fact roughly give us 1.6055, the aspect ratio we were hoping to obtain.
The second case, i.e. when we’re dealing with vertically elongated images, works out similarly. In this case the original image’s aspect ratio is less than that of the thumbnail, and we need to find out the padded image height instead of the width. Assuming we’re dealing with a 300×700 image, then:
Dividing the new height, 481.65, by 300 roughly gives us the aspect ratio we wanted.
For both cases, once we manage to fit the original image onto a bigger canvas with the right aspect ratio, then it can be resized right down to the thumbnail dimensions without losing quality.
PIL Proof of Concept
To see the above concepts in action, let’s implement them using the Python Image Library (PIL). First, make sure you have it installed:
pip3 install pillow
Then, the following code generates thumbnails for horizontally elongated images:
from PIL import Image
thumb_w = 175
thumb_h = 109
with Image.open('input.jpg') as input_image:
orig_w, orig_h = input_image.size
orig_aspect = (orig_w / orig_h)
thumb_aspect = (thumb_w / thumb_h)
if orig_aspect > thumb_aspect: # horizontal elongation - pad vertically
new_w = orig_w
new_h = int((orig_w * thumb_h) / thumb_w)
with Image.new( 'RGB', (new_w, new_h), (0, 255, 255)) as output_image: # cyan background
# y-position of original image over padded image
orig_y = int((new_h / 2) - (orig_h / 2))
# copy original image onto padded image
output_image.paste(input_image, (0, orig_y))
# resize padded image to thumbnail size
output_image = output_image.resize((thumb_w, thumb_h), resample=Image.LANCZOS)
# save final image to disk
output_image.save('output.jpg')
else: # vertical elongation - pad horizontally
pass # ...
Based on the calculations in the previous section, the code compares the aspect ratio of the original image to that of the desired thumbnail dimensions to determine whether it needs to pad vertically or horizontally. For the first case (pad vertically), it calculates the padded image height (new_h) and creates a new image to accommodate it (again, the cyan background is just to demonstrate the effect). It then copies the original image into the middle of the new image. Finally, it resizes the new image to thumbnail size, and saves it to disk:
For the second case (pad horizontally), the code is mostly the same, except that we calculate the padded image width (new_w) instead of the height, and we calculate the x-position (orig_x) when placing the original image in the middle of the new image:
else: # vertical elongation - pad horizontally
new_w = int((thumb_w * orig_h) / thumb_h)
new_h = orig_h
with Image.new( 'RGB', (new_w, new_h), (0, 255, 255)) as output_image: # cyan background
# x-position of original image over padded image
orig_x = int((new_w / 2) - (orig_w / 2))
# copy original image onto padded image
output_image.paste(input_image, (orig_x, 0))
# resize padded image to thumbnail size
output_image = output_image.resize((thumb_w, thumb_h), resample=Image.LANCZOS)
# save final image to disk
output_image.save('output.jpg')
Applying this to a vertically-elongated image, we get something like this:
This code is just a quick-and-dirty proof of concept. It can be simplified and may need adjusting to account for off-by-one errors, cases where the aspect ratio already matches, images that aren’t JPG, etc. But it shows that the calculations we’ve seen actually work in practice and produce the desired result.
Conclusion
In this article we’ve discussed the need to produce thumbnails that both conform to a desired size and retain the original image’s aspect ratio. In cases where resizing breaks the aspect ratio, we can pad the original image before resizing in order to maintain the aspect ratio.
We’ve seen how to generate padded image thumbnails using ImageMagick, and then delved into how we could do the same thing ourselves. After demonstrating the mathematical calculations necessary to create the right padding to preserve the aspect ratio, we then applied them in practice using PIL.
I learned the above techniques while trying to find a way to automatically generate decent-looking thumbnails for maps in my upcoming Ravenloft: Strahd’s Possession walkthrough, where the maps come in all shapes and sizes and some of them needed a little more attention due to elongation. Hopefully this will be useful to other people as well.