Image beautifier in Python

·

5 min read

To make a better reading experience for your audience, using images is one of the best ways. I intend to use this blog to write about my ideas. To document courses I have gone through. To report the technical problems I have faced. To clarify my points, sometimes, it is necessary to add images to my writing.

I want all the images displayed on the blog to be consistent. I want to add a background to all my images. And the process to do this needs to be as simple as possible. I don't want to go to my image processor every time I need to add an image.

So let's take a screenshot of a terminal and add a background to it. We are going to use Python and Pillow for this, so let's first import the library.

from PIL import Image

Let's open up the image I have using the library.

im = Image.open("terminal.png")
display(im)

png

Now let's add a background to this image. To do that, we need to create a new image and paste our screenshot image. I will make an image where the size is 50% larger than the original width and height. And calculate an offset, so our screenshot is placed right in the middle.

def generate_ss(im):
    # Get the current image size
    img_width, img_height = im.size

    # Make new size with 50% more for the width and height
    bg_size = (img_width + (img_width // 2),
               img_height + (img_height // 2))

    # Calculate offset
    offset = ((bg_size[0] - img_width) // 2,
              (bg_size[1] - img_height) // 2)

    im_with_bg = Image.new("RGB", bg_size, "#c7ecee")
    im_with_bg.paste(im, offset, mask=im)

    return im_with_bg
new_img = generate_ss(im)
display(new_img)

png

A note about im_with_bg.paste(im, offset, mask=im) line. When you take a screenshot on your Mac to capture a window (using Command+Shift+4 and Space bar), you got an image that has a drop shadow and transparency on the surrounding box. If we don't use the mask keyword, the image will show up with a black box surrounding the image. You can see the black box because the image has transparency. And what we want is to create a composite image that blends the background and our image. That is why we need to use the mask keyword. Let me show you what it will look like if we don't use the mask keyword.

bg_size = (im.size[0] + (im.size[0] // 2),
           im.size[1] + (im.size[1] // 2))
offset = ((bg_size[0] - im.size[0]) // 2,
          (bg_size[1] - im.size[1]) // 2)
im_with_bg = Image.new("RGB", bg_size, "#c7ecee")
im_with_bg.paste(im, offset)
display(im_with_bg)

png

This is not what we want.

Adding a drop shadow

So the previous image comes with a drop shadow. Which makes it looks nice. Naturally, you ask, what if we want to add a drop shadow for pictures that do not come with one.

It turns out someone already made a function for this. You can find the drop shadow function here. The original recipe does not allow for the shadow color to have transparency. Here I tweak the function a bit to add that ability.

from PIL import ImageFilter


def drop_shadow(image, offset=(5,5), background=0xffffff, shadow=0x444444, 
                border=8, iterations=3):
    """
    Add a gaussian blur drop shadow to an image.  
    image       - The image to overlay on top of the shadow.
    offset      - Offset of the shadow from the image as an (x,y) tuple. 
                  Can be positive or negative.
    background  - Background colour behind the image.
    shadow      - Shadow colour (darkness).
    border      - Width of the border around the image.  This must be wide
                enough to account for the blurring of the shadow.
    iterations  - Number of times to apply the filter.  More iterations 
                produce a more blurred shadow, but increase processing time.
    """
    # Create the backdrop image -- a box in the background colour with a 
    # shadow on it.
    totalWidth = image.size[0] + abs(offset[0]) + 2*border
    totalHeight = image.size[1] + abs(offset[1]) + 2*border
    back = Image.new("RGBA", (totalWidth, totalHeight), background)
    # Place the shadow, taking into account the offset from the image
    shadowLeft = border + max(offset[0], 0)
    shadowTop = border + max(offset[1], 0)
    back.paste(shadow, [shadowLeft, shadowTop, shadowLeft + image.size[0], 
               shadowTop + image.size[1]] )
    # Apply the filter to blur the edges of the shadow.  Since a small kernel
    # is used, the filter must be applied repeatedly to get a decent blur.
    n = 0
    while n < iterations:
        back = back.filter(ImageFilter.BLUR)
        n += 1
    # Paste the input image onto the shadow backdrop  
    imageLeft = border - min(offset[0], 0)
    imageTop = border - min(offset[1], 0)
    back.paste(image, (imageLeft, imageTop))
    return back

Now let's modify our generate_ss function.

def generate_ss(im, bg_color, **kwargs):
    # Get the current image size
    img_width, img_height = im.size

    # Make new image with 50% more for the width and height
    bg_size = (img_width + (img_width // 2),
               img_height + (img_height // 2))

    # Apply drop shadow
    shadow_settings = kwargs.get("shadow", {})
    img_shadow = drop_shadow(im, background=bg_color,
                             **shadow_settings)

    # Calculate offset
    new_img_width, new_img_height = img_shadow.size
    offset = ((bg_size[0] - new_img_width) // 2,
              (bg_size[1] - new_img_height) // 2)

    im_with_bg = Image.new("RGB", bg_size, bg_color)
    im_with_bg.paste(img_shadow, offset, mask=img_shadow)

    return im_with_bg

Okay, the function is ready. It is time to find a suitable parameter for the drop shadow. After trying multiple times, I found the setup that works.

shadow_setup = {
    "offset": (2, 8),
    "border": 68,
    "shadow": (0, 0, 0, 50),
    "iterations": 60
}

book_cover = Image.open("book.jpeg")
new_im = generate_ss(book_cover, "#c7ecee", shadow=shadow_setup)
display(new_im)

png

Summary

This article shows how easy it is to add background and drop shadow to images or screenshots you take using Python. In practice, I turn the code above into a command-line script. So every time I need to add pictures to my posts, I run this command, and I got a beautiful screenshot.