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
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:
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
We’re clever – but do we want to use that cleverness to build a full image manipulation package from scratch?
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) + outputSuffix + os.path.splitext(photo.filename) 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:
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!