Directory Monitoring and Image Format Conversion Using Python

Automatically convert images to WebP or PNG in real time using Python and directory monitoring

Ceyhun Enki Aksan
Ceyhun Enki Aksan Entrepreneur, Maker

In a previous article titled Python and FTP File Operations, I discussed file and FTP operations using Python. In this article, as a continuation of that post, I’ll explore a visual format conversion process that can be implemented using Python.

If you’re following my articles on Grav CMS or already using this content management system, as you know, you must first create a Markdown file (for example, post.tr.md) inside the posts directory or another relevant directory to publish content1. At this stage, if an image is included in the article, the image itself must either be located within the content or in another designated directory2. To avoid potential issues arising from naming conflicts or version differences, I generally store each image within the directory it belongs to and call it using a custom shortcode.

[raw]<ResponsiveImage
  src="image-name"
  alt="ALT"
  uri="python-watchdog-image-convert"
  caption="CAPTION"
/>[/raw]

The specified src value is then retrieved by the shortcode in png, jpg, and webp formats, and after rendering, it takes the following form:

<figure class="figure mt-0 mb-3 mx-0 img-responsive">
  <picture>
    <source data-srcset="image-name.jpg" type="image/jpeg">
    <source data-srcset="image-name.png" type="image/png">
    <source data-srcset="image-name.webp" type="image/webp">
    <img src="image-name.jpg" data-src="image-name.jpg" alt="ALT" class="lazy img-responsive img-fit-cover" title="ALT">
  </picture>
  <figcaption class="figure-caption text-tiny text-gray">CAPTION</figcaption>
</figure>

Of course, during the process of creating these images, compression and format conversion operations are involved. For compression, I use the ImageOptim3 application and the following commands. You can refer to my previous article titled Optimizing Images with jpegoptim and optipng for more details.

jpegoptim -q 80 /Users/user/Desktop/gorsel-adi.png
optipng -o7 -preserve /Users/user/Desktop/gorsel-adi.png

For converting images to the webp format, I use the cwebp command. For more detailed information on this process, you can refer to my article titled What is WebP? How to Use It?.

cwebp -q 90 /Users/user/Desktop/gorsel-adi.png -o /Users/user/Desktop/gorsel-adi.webp

As can be seen, after a series of commands, the article is ready for publication. Now, how can I streamline and automate this process more efficiently?

Python Watchdog and Pillow usage
Python Watchdog and Pillow usage

Python Watchdog Library

The Python programming language features an excellent Python API library named Watchdog for monitoring file system events (such as creation, update, deletion, etc.)4. Additionally, it’s also possible to use this library via a command-line interface.

As I gather more information on this topic, I will continue to add new examples. For now, let’s explore how we can make the image conversion process more efficient.

First, let’s create a directory and monitor its contents. In this case, Watchdog provides us with the ability to establish relationships through the FileSystemEventHandler class, specifically for the events on_created, on_deleted, on_modified, and on_moved. Additionally, you can observe all events triggered by on_any_event, which can be caught via the class.

from watchdog.events import FileSystemEventHandler
import time
from watchdog.observers import Observer

class convImage(FileSystemEventHandler):
    def on_any_event(self, event):
        print("EVENT")
        print(event.event_type)
        print(event.src_path)
        print()

flToTrack = '/Users/user/Desktop/Old'

evHandler = convImage()
observer = Observer()
observer.schedule(evHandler, evHandler.flToTrack, recursive=True)
observer.start()

try:
  while True:
    time.sleep(10)
except KeyboardInterrupt:
  observer.stop()

observer.join()

With this code snippet, we can begin monitoring the directory specified by flToTrack. Whenever you add any file into the directory, the on_any_event function will provide you with the following information:

EVENT
created
/Users/user/Desktop/Old/gorsel-adi.png

EVENT
modified
/Users/user/Desktop/Old

EVENT
modified
/Users/user/Desktop/Old/gorsel-adi.png

Now, let’s use these details to monitor a directory and, if a file with a .png or .jpg extension is added to its contents, move these files to a different directory, converting them into one another and into the webp format.

Python Pillow Library

The Pillow library is essentially a fork of PIL. With Pillow, you can perform various operations such as resizing, color conversion, coordinate manipulation, cropping and pasting, and more5.

Image.open('gorsel-adi.png').convert('RGB').save('gorsel-adi.jpg', 'jpeg')
Image.open('gorsel-adi.png').convert('RGB').save('gorsel-adi.webp', 'webp')

In addition to image resizing operations, steps such as compression and renaming can also be observed at this stage. Of course, Pillow is not mandatory. Image operations can also be performed using shell commands via os.system().

import os
if os.path.isfile(event.src_path):
  os.system(f'cwebp -q 90 {event.src_path} -o {event.src_path}.webp')

Now, let’s summarize all the above processes and put them together into a cohesive workflow.

#!/usr/bin/env python3

import os
from watchdog.events import FileSystemEventHandler
import time
from watchdog.observers import Observer
from PIL import Image

class convImage(FileSystemEventHandler):

    def __init__(self):
        self.flToTrack = '/Users/user/Desktop/Old'
        self.flDestination = '/Users/user/Desktop/New'

    def on_modified(self, event):

        for flFullName in os.listdir(self.flToTrack):

            fName, fExt = os.path.splitext(flFullName)

            if fExt == '.png':
                try:
                    Image.open(flFullName).convert('RGB').save(
                        f'{self.flDestination}/{fName}.jpg', 'jpeg')
                except Exception as err:
                    print(err)
            elif fExt in ['.jpg', '.jpeg']:
                try:
                    Image.open(flFullName).convert('RGB').save(
                        f'{self.flDestination}/{fName}.png', 'png')
                except Exception as err:
                    print(err)

            try:
                Image.open(flFullName).convert('RGB').save(
                    f'{self.flDestination}/{fName}.webp', 'webp')
            except Exception as err:
                print(err)

            src = f'{self.flToTrack}/{flFullName}'

            try:
                os.rename(src, f'{self.flDestination}/{flFullName}')
            except Exception as err:
                print(err)

if __name__ == "__main__":

evHandler = convImage()
observer = Observer()
observer.schedule(evHandler, evHandler.flToTrack, recursive=True)
observer.start()

try:
while True:
time.sleep(10)
except KeyboardInterrupt:
observer.stop()

observer.join()

You can convert this code snippet into an alias and run it via a command-line interface. I briefly discussed this topic in my article titled Python and FTP File Operations. Additionally, you can access the updated version of the above code snippet at convImage.py, and feel free to share your suggestions in the comments.

*[PIL]: Python Imaging Library

Footnotes

  1. Content. Grav
  2. Media. Grav
  3. ImageOptim. ImageOptim makes images load faster
  4. Watchdog. Python API library for file events
  5. Pillow Handbook