Compressing Images And Managing Thumbnails In Django Admin

by hash3liZer . 24 June 2018

Compressing Images And Managing Thumbnails In Django Admin

Images are always a pain in the butt when it comes to the speed of the site. This would present a lot of problems for visiting users and eventually would decrease the users' loyalty. Hence, will leave a negative impact on visitors. What we could do is to compress these images to their lowest possible size either by decreasing their pixel quality or resizing them depending on the container's width.

For Django, this would be a little tricky for we have to do it while the image is getting saved on the server. Another option, you got is Javascript which sure must not be an option as it's the matter of turning it off. What exactly we will be doing is editing the Super Class method save of our Subclass model to detect the presence of the image and then we will define the image compression logic. Just digest the previous statement, we will see how in subsequent steps.

Well, reducing picture quality could be a real easy process when comes to third-party services as we got https://www.thecanvasprints.co.uk/ at the moment. We will use Pillow (PIL) library to create thumbnails and modify images. Now, this would require you the be familiar with Pillow library. I would suggest reading the pillow documentation. But now considering the shortage of time, process the below kick-start introduction:

A simple Pillow program

from PIL imort Image

img = Image.open('image.jpg')
img.convert( 'RGB' )
img.save( '/path/to/file.jpeg', format="JPEG", quality=80 )
img.close()

This was the pretty simple image program. We opened an image file, converted into RGB mode, save it on the disk with JPEG format and reduced it's quality to 80. After which we closed the file.

Some other Pillow behaviors:

img1 = img.copy()
img.paste(img1)

This first one copy the image to variable image 1 while the second one paste back the data of img in img1.

img.resize( (width, height) )

This will resize the image to the provided width and height values.

img.thumbnail( (width, height) )

This will create thumbnail of an image. It's working is similar to resize(). The difference is that resize method perform operations on the actual image and commit the required changes. While thumbnail returns an instance of the modfied image on which further actions are done.

Take a head-start...

Whenever the Model instance change or any sort of alteration is encountered by the django workers, the save method from models.Model base class is executed. This method provides us the neccessary scope to alter the data before it gets saved on the database.

Come to your application models.py file and create a new model with two Image fields. Subsequently, subclass the save method from model.Model super class.

from django.models import models

class Images(models.Model):
    image1 = models.ImageField( upload_to="images/" )
    image2 = models.ImageField( upload_to="images/", null=True, blank=True, editable=False )

    def save(self, *args, **kwargs):
        if self.image1:
            <...snipped...>

We created a non-editable extra image field (image2) in the model. This field will not be presented to user and initially be blank when save method is encountered. I created this extra field merely to store a thumbnail of different size. However, we are not going to mess with this right now neither we are gonna see it's usage in the rest of this tutorial. It's for you to test if you are able to do it perfectly as required.

Now, run migrations command and register the model to Admin Interface...

python manage.py makemigrations
python manage.py migrate

admin.py:

from django.contrib import admin
from . import models

admin.site.register( models.Images )

So, now we have our environment setup. Come back to the models.py and let's see how we can compress images.

Remember that save method from the superclass models.Model will be executed everytime, if any of database change is committed with the exception of update method. If you are dealing with a large database or something that pretty big enough to mess with speed of the site, make sure you do know the use of update method. It will help in many of such cases where you want to update a field without calling the save method.

Coming towards save method, it gets called when the model fields are going to save. At this part, we can check and make changes as required to the populated fields. Lets see it a little closer:

from PIL import Image as PILImage
from StringIO import StringIO

class Images(models.Mode):
    ...

    def save(self, *args, **kwargs):
        if self.image1:
            img = PILImage.open( StringIO( self.image1.read() ) )
            if img.mode != "RGB":
                img.convert('RGB')
            width, height = img.size    #1
            if width > 640 or height > 480:
                width, height = 640, 480
            img.resize( (width, height), PILImage.ANTIALIAS )   #2
            save_buff = StringIO()    #3
            img.save( save_buffer, format="JPEG", optimize=True, quality=70)   #4

        super( Images, self ).save( *args, **kwargs )

Above in the code, We first opened the file in a buffer and read image data from this memory block. We used buffer here because we are going to make changes to, let's say pretty large binary data and buffers are very useful while dealing with such data. At the second part, we converted the image into RGB mode. Further note these steps:

  1. We definitely will require a size for our image. Atleast for sure, so it gets fit into a container. Go on and quickly read this table for various common used resolution sizes for images.
  2. We resized the image to the required size.
  3. We created a buffer for the image.
  4. We saved to image to the buffer. Note that optimize will try to reduce the image to lowest possible size.

Saving the Image

Now, it's all just the matter of saving image to disk. Until now, our image is placed in a memory buffer. To save the image, we will use django InMemoryFileUploadedFile. Let's see it a more lively...

from PIL import Image as PILImage
from StringIO import StringIO
from django.core.files.uploadedfile import InMemoryUploadedFile

class Images(models.Model):
    ...

    def save(self, *args, **kwargs):
        if self.image1:
            img = PILImage.open( StringIO( self.image1.read() ) )
            if img.mode != "RGB":
                img.convert('RGB')
            width, height = img.size
            if width > 640 or height > 480:
                width, height = 640, 480
            img.resize( (width, height), PILImage.ANTIALIAS )
            save_buff = StringIO()
            img.save( save_buffer, format="JPEG", optimize=True, quality=70)

            self.image1 = InMemoryUploadedFile( save_buff, 'ImageField', self.Image.name.split( '.' )[0], 'image/jpeg', raw_space.len, None )
        
       super( Images, self ).save( *args, **kwargs )

Now, everything's done. You can now test it. This is the before and after of an image is uploaded:

img

So, that's how we compressed the image using a few lines of code in Python. Well, this is for Django. But now you'll be able to implement the image compressions logic in your other programs as well.

Extra Field

We created two image fields. If you'd read all of this which I bet you didn't, I said I created that another field merely to store another copy of the uploaded image, i.e. of a different size or resolution. I'll leave that to you. Resize the image to 320x200 pixels and save that another copy to the image2 field and test yourself, if you really be able to do it or not.