Resize tons of photos in Linux with Python

I have the library of photos taken during the last 20+ years. I have to resize all of them to make smaller copies which will load faster through the GSM connection on my phone when I’m in remote locations. Of course, I want to keep also larger versions of my photos. Originals will stay in the place while smaller versions will be placed in the new folder. In my archive, I created the separate folder for each year. In each of these folders, I created sub-folders to divide my photos into events. This way it is easy for me to find the proper set of pictures – names of the folders are rather descriptive. I wanted to keep this folder structure.

Goals

My goals for this resizing project were as follows:

  • resize over 150.000 photos with minimal effort
  • leave originals in the place
  • duplicate source directory structure at the destination
  • resize only particular types of images
  • I want to be able to break the process at any time and restore it later
  • I want to be able to repeat the process for newly uploaded photos

Programming language selection

I was thinking about the regular bash script. I think that there is nothing that hard in the set of tasks it should do. On the other hand, I don’t like bash scripts. I’m not feeling competent and I’m not sure if I will not make some catastrophic mistake somewhere in my code. I’m also not sure how to deal with data structures in bash so I have chosen the easier way.

Python is also new to me, but I feel more comfortable and I feel that I have more control over the development process and over the code. It’s much easier for me to dump variables and review them before I will use them somewhere. Python code is also easier for me to read and there are a lot of examples I can review and use. Sometimes all things I need are already there on the net, all I need to do is to stitch them together into working piece.

Of course, there are also drawbacks – I had no pillow (PIL) library installed to resize images. I had also no pip installed to install this library – so I had to google all answers I needed. The same with exceptions – I noticed that my script is breaking at one of the images. I had to implement exception handling and I had to handle the proper exception, not all of them. I wanted to be able to break the script execution using the keyboard so I had to leave keyboard based exceptions unhandled or handle separately.

The script itself

The script below will work with Python 3, with the PIL or pillow module installed.

import os
import sys
from PIL import Image

sourceDir = "/path/to/source/directory/"
destinationDir = "/path/to/destination/directory/"
allowedExtensions = [".jpg",".png",".gif",".jpeg"]

def scale_image(input_image_path, output_image_path, width=None, height=None):
  original_image = Image.open(input_image_path)
  w, h = original_image.size
  print('The original image size is {wide} wide x {height} '
      'high'.format(wide=w, height=h))

  if width and height:
    max_size = (width, height)
  elif width:
    max_size = (width, h)
  elif height:
    max_size = (w, height)
  else:
    # No width or height specified
    raise RuntimeError('Width or height required!')

  original_image.thumbnail(max_size, Image.ANTIALIAS)
  original_image.save(output_image_path)
  print('Resised, saved')


for dirname, dirnames, filenames in os.walk(sourceDir):
  # create destination directories if not present
  for subdirname in dirnames:
    thisSrcDir = os.path.join(dirname, subdirname)
    thisDstDir = thisSrcDir.replace(sourceDir,destinationDir)
    if not os.path.exists(thisDstDir):
      os.makedirs(thisDstDir)
      print('Created: ',thisDstDir)

  # copy and resize file if not present
  for filename in filenames:
    thisFullFilename = os.path.join(dirname, filename)
    #check the extension
    thisExt = os.path.splitext(thisFullFilename)[1].lower()
    if thisExt in allowedExtensions:
      thisFullDestinationFilename = thisFullFilename.replace(sourceDir,destinationDir)

      #check if destination file present
      if not os.path.exists(thisFullDestinationFilename):
        #copy and resize
        print('Copying and resizing file to: ',thisFullDestinationFilename)
        try:
          scale_image(thisFullFilename,thisFullDestinationFilename,1920,1280)
        except OSError as err:
          print("Error during scale: {0}".format(err))
        except KeyboardInterrupt:
          exit()