In Django model: save an image with Pillow (PIL) library
Tags: django, pillow
Published: 2024-06-28 09:28:46

### Introduction to the `save` Method

The `save` method in Django models is designed to persist the model instance to the database. By default, it handles basic saving operations, but it can be overridden to add additional functionality or to customize the saving process. In this `Picture` model, we have overridden the `save` method to include specific image handling logic before the actual save operation is performed.

#### Purpose

The primary purpose of this custom `save` method is to:
1. **Validate the Image:** Ensure that the uploaded file is a valid image.
2. **Process the Image:** If necessary, convert and resize the image to meet specific criteria.
3. **Optimize the Image:** Save the image in a specific format with optimized settings.

#### Necessity

Handling image files can be tricky because they come in various formats and sizes. Furthermore, user-uploaded images might not always be in the desired format or resolution. This custom logic ensures that:
- The image is verified and valid.
- The image is processed to maintain a consistent format (JPEG).
- The resolution of the image is controlled to optimize load times and storage requirements.

#### Overview of the `save` Method Operation

1. **Image Verification:** The method first tries to open and verify the uploaded image to ensure that it is a valid image file.
   
2. **Reopen Image:** Since the `img.verify()` method moves the file pointer to the end, the image file is reopened to reset the pointer.

3. **Image Mode Adjustment:** If the image is in a mode other than RGB, such as RGBA or P, it is converted to RGB. This is often done to standardize the image format, as certain modes like RGBA include alpha channels which might not be needed.

4. **Resizing the Image:** The image is resized to a width of 800 pixels while maintaining the aspect ratio. This ensures that all images have a consistent width, which can be important for display purposes and performance optimization on the website.

5. **Saving the Image:** The processed image is then saved as a JPEG file with a specific quality setting. The new file is temporarily stored in memory using `BytesIO` before being saved to the model's `photo` field.

6. **Exception Handling:** If any errors occur during these processes (either during verification, reopening, or processing), specific exceptions are raised with meaningful error messages. This helps in diagnosing issues with uploaded files.

Finally, the overridden `save` method calls `super().save(*args, **kwargs)` to ensure that the default saving behavior is executed, and the model instance is saved to the database.

By implementing this custom save method, we ensure better control and consistency over how images are handled and stored, thus improving the robustness and reliability of the application.

---


```python
# models.py 

class Picture(models.Model):
    legend = models.CharField(max_length=100)
    photo = models.ImageField(
        upload_to="images/",
        blank=True,
        null=True,
    )
    published = models.BooleanField(default=True)

    def __str__(self):
        return self.legend

    def save(self, *args, **kwargs):
        if self.photo:
            try:
                img = Image.open(self.photo)
                img.verify()
                # reopen because img.verify() moves pointer to the end of the file
                img = Image.open(self.photo)

                # convert png to RGB
                if img.mode in ("RGBA", "LA", "P"):
                    img = img.convert("RGB")

                # Calculate new dimensions to maintain aspect ratio with a width of 800
                new_width = 800
                original_width, original_height = img.size
                new_height = int((new_width / original_width) * original_height)

                # Resize the image
                img = img.resize((new_width, new_height), Image.LANCZOS)

                # Prepare the image for saving
                temp_img = BytesIO()
                # Save the image as JPEG
                img.save(temp_img, format="JPEG", quality=70, optimize=True)
                temp_img.seek(0)

                # Change file extension to .jpg
                original_name, _ = self.photo.name.lower().split(".")
                img = f"{original_name}.jpg"

                # Save the BytesIO object to the ImageField with the new filename
                self.photo.save(img, ContentFile(temp_img.read()), save=False)

            except (IOError, SyntaxError) as e:
                raise ValueError(f"The uploaded file is not a valid image. -- {e}")

        super().save(*args, **kwargs)
```