Building a Python Script to Resize & Watermark Images [Code Included]

Building a Python Script to Resize Watermark Images

In this project, we take you through building, packaging, and publishing a command-line application using Python to resize and watermark images.

Whenever I complete an article for LinuxScrew, I need to go through and add watermarks and make other small adjustments to any photos I’ve taken. This can be tedious so I thought, why not automate it. Then I thought, why not make a command-line app to do it (rather than just a bash script), and show you how you can make your own command-line apps, too?

We’ll be writing our app logic in Python, and using the Python package Click to add a tidy command-line interface, and then wrapping it all up into a standalone executable for Linux using PyInstaller.

There’s plenty of code ahead, but I’ll keep things as simple as possible. By the end of this article, you’ll be able to see how to create a full Python application for your computer that can be distributed and run by your friends or colleagues. It won’t be just like a full professionally built software package – it will be one!

Setting Up Your Development Environment

Follow our article on setting up the pip package manager to get started!

Planning Your App

So you want to build a command-line app – first, you have to figure out what to call it. Let’s go with something LinuxScrew appropriate:

photoScrewer

Now we need to figure out exactly what it needs to do. Don’t build an app without knowing what it needs to achieve. Without a purpose, you’ll never get anything done and hate yourself forever.

In our app, we want the user to be able to:

  • Set the directory containing the photos/images to be processed
    • Saved modified photos should have a suffix so the originals aren’t destroyed
  • Resize an image to fit a given width and/or height
    • Resize proportionally
  • Add an optional watermark to the image
    • This should also be resizable as above
    • The user should also be able to specify a margin so that it’s not touching the edge of the photo
    • The user should also be able to decide which corner of the photo the watermark appears
  • Remove all EXIF Data
    • This removes location data from your photos so the boss is none the wiser that you’ve been working from Prague for the last 3 weeks

Let’s translate that to some command-line options with appropriate names:

photoscrewer -photo-folder-path=[path to photos folder] --resize-width=[number] --resize-height=[number] --watermark-image-path=[file path] --watermark-width=[number] --watermark-height=[number] --watermark-position=[topLeft, topRight, bottomLeft, bottomRight]

[number] will refer to the number of pixels

When you run the finished application, typing the above into the shell will cause it to jump into action and process your images with the given options.

Creating a Directory for your Application

mkdir projectPhotoScrewer
cd projectPhotoScrewer

Installing Dependencies

We’re clever – but do we want to use that cleverness to build a full image manipulation package from scratch?

No.

Not because we can’t, but because we don’t have to because other helpful and much cleverer people have already done it, and made the code public for us to use.

The Pip package manager gives you access to thousands of packages – for everything from accepting commands from the command line to manipulating images – which is good because that’s what we need to do.

Install the three dependencies for this project using the command below:

pip3 install pillow click pyinstaller

Coding Your App

Looking back at our plan above, this app performs four main tasks, so we’ll write these as functions in Python. Later, values from the command line will be passed to them to create a functioning application.

Well written code speaks for itself (and contains comments so you can remember why you did what you did), so here it is:

# Dependencies: 
# pillow - A python image manipulation library
# click - Turns your python script into a command line application 
# pyinstaller - Packages the application for distribution

# This is designed for Python version 3 only

# To package this app for distribution, run
# python3 -m PyInstaller photoScrewer.py

# Import required packages
from PIL import Image
import glob
import click
import os

# Global Variables and their defaults

# List of images being processed
photoList = []

# Output dimensions in pixels - maximum width, maximum height
# Processed photo will be set to proportionally fit inside these dimensions
size = 1080, 1080

# Output file suffix
outputSuffix = "-screwed"

# Watermark location and dimensions.  Watermark will be scaled to proportionally fit these dimensions
watermarkPath = "watermark.png"
watermarkSize = 128, 128
watermarkPosition = "bottomRight" 
# "topLeft", "topRight", "bottomLeft" or "bottomRight" - where the watermark will be placed in the photo
validWatermarkPositions = "topLeft", "topRight", "bottomLeft", "bottomRight"

# Image Processing Functions: 
# Functions must be defined BEFORE they are used

# Function to resize a photo
def resizePhoto(photo):

    print('Resizing ' + photo.filename)
    photo.thumbnail(size, Image.ANTIALIAS)# The thumbnail method will resize the image proportionally

# Function to add a watermark to a photo
def watermarkPhoto(photo):

    print('Watermarking ' + photo.filename)
    watermark = Image.open(watermarkPath)

    # Resize watermark
    watermark.thumbnail(watermarkSize, Image.ANTIALIAS)

    # Auto detect watermark margin as 3% photo width
    margin = int(round(photo.width * 0.03)) # must be an integer number of pixels
    
    # Get the variables for calculating the watermark size and position
    photoWidth, photoHeight = photo.size
    watermark_width, watermark_height = watermark.size

    topLeft = (0 + margin, 0 + margin)
    topRight = (photoWidth - margin - watermark_width, 0 + margin)
    bottomLeft = (0 + margin, photoHeight - margin - watermark_height)
    bottomRight = (photoWidth - margin - watermark_width, photoHeight - margin - watermark_height)

    position = bottomRight # Default position

    # Only assign the position from user input if it is valid
    # You should never use eval with unfiltered user input - they could do some damage!
    if watermarkPosition in validWatermarkPositions:
        position = eval(watermarkPosition) # eval will get the variable name from the users selection

    # First param is the image to paste, second is a 4-tuple describing the region to paste into, third param is the mask image
    # The mask is required as it sets the regions to be updated by the paste - making transparency happen!
    photo.paste(watermark, position, watermark)

# Function to save the photo.
# Saving the image will also strip it of EXIF data, which is the default behavior of the pillow library
def savePhoto(photo):

    print('saving ' + photo.filename)
    
    # Create the new file name by using os.path.splitext() to remove the file extension, insert the output suffix, then add the extension back on to the end
    # os.path.splitext() returns an array of strings - the first item is the filename without the file extension, and the second item in the array is the extension
    saveFileName = os.path.splitext(photo.filename)[0] + outputSuffix + os.path.splitext(photo.filename)[1]
    outputFormat = photo.format # Save the photo in its original format
    try:
        photo.thumbnail(size, Image.ANTIALIAS)
        photo.save(saveFileName, outputFormat)
    except IOError as err:
        print("cannot create file for for '%s'" % photo.filename)
        print(err) # Print the error itself for debugging

# Placing @click.command() decorator in front of a function makes it callable from the command line with the defined options!

@click.command() 
@click.option('--photo-folder-path', default=".", help='Path to your photos folder', type=click.Path())
@click.option('--resize-width', default=1080, help='Resize width', type=int)
@click.option('--resize-height', default=1080, help='Resize height', type=int)
@click.option('--watermark-image-path', default="watermark.png", help='Path to your watermark', type=click.Path())
@click.option('--watermark-width', default=128, help='Watermark width', type=int)
@click.option('--watermark-height', default=128, help='Watermark height', type=int)
@click.option('--watermark-position', default="bottomRight", help='Watermark position', type=str)

# Function to read the photo directory, process the images using the below functions, and save them
def processPhotos(photo_folder_path, resize_width, resize_height, watermark_image_path, watermark_width, watermark_height, watermark_position):

    # Note that the click options are converted to variables with underscores which we will assign to global variables

    # Get the photos in the directory and put them in a list
    global photoPath
    photoPath = photo_folder_path
    global photoList
    photoList = [Image.open(item) for i in [glob.glob(photoPath + '/*.%s' % ext) for ext in ["jpg","gif","png","tga"]] for item in i]
    
    # Update global variables with users options
    # The variables are declared with the 'global' keyword to ensure the global variable defined earlier in this file is updated with the new value
    global size
    size = resize_width, resize_height
    global watermarkPath 
    watermarkPath= watermark_image_path
    global watermarkSize
    watermarkSize = watermark_width, watermark_height
    global watermarkPosition
    watermarkPosition = watermark_position

    # Process each image in the list using the image processing functions defined above
    for photo in photoList:

        print('Processing ' + photo.filename)
        resizePhoto(photo)
        watermarkPhoto(photo) # We'll do this after resizing the photo so size is consistent
        savePhoto(photo)

    print('Done!')

# Set the main click command to trigger processPhotos()
if __name__ == '__main__':
    processPhotos()

# End of File - That's all folks!

That was quick! We’ve leaned as heavily as possible on the Pillow library to do the work and calculating. This isn’t lazy, it’s smart. No need to re-invent the wheel.

If you want to expand on what I’ve included above, the documentation for the dependency packages is written in plain English and covers the full functionality of each package:

https://click.palletsprojects.com/en/7.x/

https://pyinstaller.readthedocs.io/en/stable/

https://pillow.readthedocs.io/en/stable/

Publishing With PyInstaller

To package your application with PyInstaller, run the following in the project directory:

python3 -m PyInstaller photoScrewer.py

This is too easy. The application is now packaged in the dist directory, created by Pyinstaller – you can test it by navigating to the packaged code and running it

cd dist/photoScrewer
./photoScrewer --help

./ tells the Linux shell to execute the executable application photoScrewer in the current directory

Ah! Your application ran and printed the help info to the command line!

AND – you didn’t have to use Python to execute your photoScrewer package – it’ all self-contained. You can zip the dist directory up, ready for distribution.

Distributing Your App

With your app built, you can now distribute it to your friends. Unlike basic Python (or Bash) scripts, they won’t have to install Python or any dependencies – everything is included and bundled up in your app. Your users won’t even have to know what Python is to use your program on their Linux machines!

This doesn’t just work for command-line apps – you can build full apps with graphical interfaces in Python and you can package them the same way for a full “I’m a real software developer” product.  PyInstaller can also create packages for Windows and Mac if needed!

Click here for more fun Linux hardware and software projects!

SHARE:
nv-author-image

Brad Morton

I'm Brad, and I'm nearing 20 years of experience with Linux. I've worked in just about every IT role there is before taking the leap into software development. Currently, I'm building desktop and web-based solutions with NodeJS and PHP hosted on Linux infrastructure. Visit my blog or find me on Twitter to see what I'm up to.

Leave a Reply

Your email address will not be published. Required fields are marked *