05 February 2020

Suppose we have a very simple Post model which will be an image and its description as

from django.db import models class Post ( models . Model ): text = models . TextField () image = models . ImageField ( upload_to = 'images/' )

But we want to optimize the image size, which will be pointed to by the image field of our Post . There are good reasons to do this ‐ it helps to load the website/app faster and reduces our server storage. Before working with Django, lets first go over a brief overview of image compression using Pillow.

Compressing images using Pillow

Pillow is an excellent Python package for image-related operations. The Image class comes with methods for image io and manipulation. The Image.open reads an image from either file path or a file object. The save method of Image class takes quality as an optional parameter for saving an image in jpg format, which can range from 1 to 95, the default value for this parameter is 75, and setting quality more than 95 results in the image size more than the original.

from PIL import Image im = Image . open ( '/some/path/to/image' ) im . save ( '/desired/path/new_image_name.jpg' , quality = 70 ) im . close ()

Using quality argument is not the only way in which you can get a reduction in size. For example, You can combine it with the resizing of the image to get even small image sizes.

Utilizing the power of Django signals

signals allow certain senders to notify a set of receivers that some action has taken place.

Django comes with many built-in signals, and currently, we are interested in the django.db.models.signals.pre_save signal, which will be sent before calling save() method of the model. To connect a handler to a signal, there is Signal.connect method. To attach signal to a particular sender(model in our case), we have to give a sender argument to the Signal.connect method, for example, we attach pre_save signal to our Post model (defined above) as

pre_save.connect(our_handler, sender=Post)

Django also provides the receiver decorator for connecting to signals, which makes the code more idiomatic. So instead of defining our_handler and then connecting it, we can decorate the definition of our_handler as

from django.dispatch import receiver ... @receiver ( pre_save , sender = Post ) def my_handler ( sender , ** kwargs ): ...

Lets now complete our handler to compress image. The pre_save signal also sends the instance argument to the handler function, which corresponds to the actual instance being saved. This is particularly useful when we want to check if a field has been updated, as we don't want to compress the image repeatedly. So we can have the handler function as

from django.db.models.signals import pre_save from django.dispatch import receiver @receiver ( pre_save , sender = Post ) def handle_image_compression ( sender , instance , ** kwargs ): try : post_obj = Post . objects . get ( pk = instance . pk ) except Post . DoesNotExist : # the object does not exists, so compress the image instance . image = compress_image ( instance . image ) else : # the object exists, so check if the image field is updated if post_obj . image != instance . image : instance . image = compress_image ( instance . image )

Now the last task for us is to write the compress_image function, which will take an ImageField and returns an ImageField . The Image.open() method of PIL can only be used for file path or file object. Here is the interesting fact, the FileField , which is the super class of ImageField , mirrors the python's File API, and thus, we can use it as if it were an actual file. The problem of using Image.open is resolved, but what about Image.save ? It turns out that Image.save can write the image to a BytesIO object. So our function to compress the image will become

from PIL import Image from io import BytesIO from django.core.files import File def compress_image ( image ): im = Image . open ( image ) out = BytesIO () im . save ( out , 'JPEG' , quality = 70 ) compressed = File ( out , name = image . name ) im . close () return compressed

That's it. If you have any doubts, please drop me an email or create an issue at tarptaeya/lambda_magazine.