29

I searched a lot on this subject but couldn't really find what I need. I'll explain my problem :

On my website, the user can upload an image. I need to resize this image and convert it to JPEG before uploading it.

I saw some solutions using a method in the Views.py, but I'd like to do it by overriding the save() method in my Models.py. The problem is, all solutions I found for resizing image in save method do it after saving it a first time (calling super fonction), which means it'd use bandwidth (if I use a CDN) for nothing (am I right on this point?).

Thank you for your help

Raphael Laurent
  • 1,941
  • 1
  • 17
  • 27
  • 3
    How is doing it in save() "before uploading it"? Anything on the Django side can only possibly have access to the image data once it's uploaded. – Daniel Roseman Jun 23 '14 at 19:01
  • The views send some parameters to the save method, right? We can probably override this method to change the parameters (in my case, the image), then call the super() fonction that will actually do the saving EDIT : by "before uploading it", I meant before uploading the Image to the CDN (so from Webserver to CDN) – Raphael Laurent Jun 23 '14 at 19:24

9 Answers9

48

First, it's best to establish the correct language. Django and Python exist only on the server side. Therefore, anything they manipulate, save, or otherwise use, has to be first sent to the server. If Django or Python is to manage the photo, the user MUST upload this photo to the server first. Once the photo is uploaded, Django is free to make changes before storing the file.

If your concern is with upload bandwidth, and you don't want large files being uploaded, you will have to resize and reformat the photo on the client side. If this is a web application, this can be done using Javascript, but can not be done with Python, since Python does not operate on the client side for an application like yours.

If your concern is not with bandwidth, then you're free to have the user "upload" the file, but then have Django resize and reformat it before saving.

You are correct that you will want to override your save function for the photo object. I would recommend using a library to handle the resizing and reformatting, such as sorl.

from sorl.thumbnail import ImageField, get_thumbnail

class MyPhoto(models.Model):
    image = ImageField()

    def save(self, *args, **kwargs):
        if self.image:
            self.image = get_thumbnail(self.image, '500x600', quality=99, format='JPEG')
        super(MyPhoto, self).save(*args, **kwargs)

Sorl is just a library I am confident and familiar with, but it takes some tuning and configuration. You can check out Pillow or something instead, and just replace the line overriding self.image.

I also found a similar question here.

Edit: saw the update to your comment response above. Also note that if your webserver is handling Django, and your files are being saved to some CDN, this method will work. The image will be resized on the webserver before being uploaded to your CDN (assuming your configuration is as I'm assuming).

Hope this helps!

Jamie Counsell
  • 7,730
  • 6
  • 46
  • 81
  • Thank you for your contribution, it helps a lot. I installed sorl, unfortunately it seems something is still wrong with my implementation, I get this error : OperationalError no such table: thumbnail_kvstore the error happens in the get_thumbnail() function :/ – Raphael Laurent Jun 23 '14 at 19:57
  • @RaphaelLaurent, as I said, Sorl does take some configuration. You need an active and configured key value store, as Sorl caches all the photos. You can see the detailed installation here: http://sorl-thumbnail.readthedocs.org/en/latest/requirements.html. May I recommend instead using Pillow (a fork of PIL) and the methods of the PIL Image class. This may be easier for you: http://pillow.readthedocs.org/en/latest/reference/Image.html#PIL.Image.Image.resize – Jamie Counsell Jun 23 '14 at 20:00
  • Yes thank you, I'll look forward into that now that I have a base :) – Raphael Laurent Jun 23 '14 at 20:03
19
from django.db import models
from django.contrib.auth.models import User
from PIL import Image



class profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.CharField(max_length=300)
    location = models.CharField(max_length=99)
    image = models.ImageField(default='default.jpg', upload_to='profile_pics')

    def save(self):
        super().save()  # saving image first

        img = Image.open(self.image.path) # Open image using self

        if img.height > 300 or img.width > 300:
            new_img = (300, 300)
            img.thumbnail(new_img)
            img.save(self.image.path)  # saving image at the same path

This example shows how to upload image after image re-sizing. Change the pixel of new_img, whatever you want.

Manish
  • 501
  • 4
  • 11
11

Here is one more package that works for me with minimal code modification - django-resized.

models.py

from django_resized import ResizedImageField

class Post(models.Model):
    image = ResizedImageField(upload_to='uploads/%Y/%m/%d')

settings.py

DJANGORESIZED_DEFAULT_SIZE = [1024, 768]
DJANGORESIZED_DEFAULT_QUALITY = 75
DJANGORESIZED_DEFAULT_KEEP_META = True
DJANGORESIZED_DEFAULT_FORCE_FORMAT = 'JPEG'
DJANGORESIZED_DEFAULT_FORMAT_EXTENSIONS = {'JPEG': ".jpg"}
DJANGORESIZED_DEFAULT_NORMALIZE_ROTATION = True

That's it!

Ramon Dekkers
  • 168
  • 2
  • 9
  • 1
    Awesome Library! I wonder why is this (django-resized) library not getting too much attention. It's so simple to implement and works amazingly! Thanks for the recommendation. – Nabajyoti Das Oct 25 '20 at 19:17
9

Here is an app that can take care of that: django-smartfields. It will also remove an old image whenever a new one is uploaded.

from django.db import models

from smartfields import fields
from smartfields.dependencies import FileDependency
from smartfields.processors import ImageProcessor

class ImageModel(models.Model):
    image = fields.ImageField(dependencies=[
        FileDependency(processor=ImageProcessor(
            format='JPEG', scale={'max_width': 300, 'max_height': 300}))
    ])
lehins
  • 9,642
  • 2
  • 35
  • 49
  • can you help here please. https://stackoverflow.com/questions/66680664/replicating-django-forms-request-files-to-upload-multiple-image-files-using-smar – Vishal Sharma Mar 18 '21 at 08:11
7

EDIT4 : full working code can be found here (the code below still has some mistakes)

Still fighting, but there is progress ^^ : I am trying to do the resizing and convertion in memory with PIL (and seeing the amount of questions on the subject, it seems that I am not the only one ^^). My code so far (in Models.py):

from PIL import Image as Img
import StringIO
from django.core.files import File

class Mymodel(models.Model):
#blablabla
photo = models.ImageField(uppload_to="...", blank=True)

    def save(self, *args, **kwargs):
        if self.photo:
            image = Img.open(StringIO.StringIO(self.photo.read()))
            image.thumbnail((100,100), Img.ANTIALIAS)
            output = StringIO.StringIO()
            image.save(output, format='JPEG', quality=75)
            output.seek(0)
            self.photo = File(output, self.photo.name())
        super(Mymodel, self).save(*args, **kwargs)

I have an error at "photo = File(output, photo.name())"

Thanks

EDIT : Sorry, it was a simple mistake (forgot the self.) corrected in code. Now I have the error "TypeError, 'unicode' object is not callable", at the same line.

EDIT2: Saw something about "output.getvalue()" here but don't really know if could be of any help.

EDIT3 : Solved!! Code below.

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

class Mymodel(models.Model):
    photo = models.ImageField(upload_to="...", blank=True)

    def save(self, *args, **kwargs):
        if self.photo:
            image = Img.open(StringIO.StringIO(self.photo.read()))
            image.thumbnail((200,200), Img.ANTIALIAS)
            output = StringIO.StringIO()
            image.save(output, format='JPEG', quality=75)
            output.seek(0)
            self.photo= InMemoryUploadedFile(output,'ImageField', "%s.jpg" %self.photo.name, 'image/jpeg', output.len, None)
        super(Mymodel, self).save(*args, **kwargs)

For PNG looking weird, see second post on this thread

Community
  • 1
  • 1
Raphael Laurent
  • 1,941
  • 1
  • 17
  • 27
  • When converting a PNG, the image is "weird" (background yellow in some places for example), but I think this problem is well documented. – Raphael Laurent Jun 24 '14 at 07:35
  • Problem solved with this thread : http://stackoverflow.com/questions/9166400/convert-rgba-png-to-rgb-with-pil – Raphael Laurent Jun 24 '14 at 07:49
  • Sorry, me again, I have one last issue : on every edit, a new image is created (the save function don't check if there is already the image saved in the model). Any clue about how avoiding that? Thank you. – Raphael Laurent Jun 24 '14 at 08:03
  • try: this = Mymodel.objects.get(id=self.id) if this.image != self.image: this.image.delete(save=False) except: pass # when new photo then we do nothing, normal case – Raphael Laurent Jun 24 '14 at 10:26
  • Wouldn’t be more appropriate to use BytesIO()? – erickfis Oct 18 '21 at 01:30
3

Just put together this for my own project, took me a while before i realized that the bytes and the image is separate attributes on the Django ImageField, this solution worked for me with Python 3.6.

    def clean_image(self):
        image_field = self.cleaned_data.get('image')
        if image_field:
            try:
                image_file = BytesIO(image_field.file.read())
                image = Image.open(image_file)
                image.thumbnail((300, 300), Image.ANTIALIAS)
                image_file = BytesIO()
                image.save(image_file, 'PNG')
                image_field.file = image_file
                image_field.image = image

                return image_field
            except IOError:
                logger.exception("Error during resize image")
LanfeaR
  • 241
  • 1
  • 8
3
from PIL import Image
class Article(TimeStampedModel):
image = models.ImageField(upload_to='article_images/', null=True, blank=True)


def save(self, *args, **kwargs):
    if self.image:
        super().save(*args, **kwargs)
        img = Image.open(self.image.path)
        if img.height > 700 or img.weight > 700:
            output_size = (700,700)
            img.thumbnail(output_size)
            img.save(self.image.path)
Gata
  • 361
  • 3
  • 4
  • 1
    Thanks for taking the time to provide an answer. It is helpful to provide an explanation of what your answer provides that's unique from the other answers (8 as of this writing). – DaveL17 Mar 27 '21 at 12:53
2

This Worked for me try it. I am using Django 2.0.6 and Python 3.6.4

from django.db import models
from PIL import Image
from io import BytesIO
from django.core.files.uploadedfile import InMemoryUploadedFile

class ImageUpload(models.Model):
    name = models.CharField(max_length=100)
    uploadedImage = models.ImageField(upload_to='Uploads/%Y/%m/', db_column="Upload an Image")
    def save(self, *args, **kwargs):
        imageTemproary = Image.open(self.uploadedImage)
        outputIoStream = BytesIO()
        imageTemproaryResized = imageTemproary.resize( (1020,573) ) 
        imageTemproaryResized.save(outputIoStream , format='JPEG', quality=85)
        outputIoStream.seek(0)
        self.uploadedImage = InMemoryUploadedFile(outputIoStream,'ImageField', "%s.jpg" %self.uploadedImage.name.split('.')[0], 'image/jpeg', sys.getsizeof(outputIoStream), None)
        super(ImageUpload, self).save(*args, **kwargs)
Ken Mbogo
  • 181
  • 2
  • 3
  • This was exactly what I was looking for, I just specified max width and height, then add a ratio `r = min( (max_w / actual_w), (max_h / actual_h) )` to it and did the resize like this: `resize( (actual_w * r, actual_h * r) )`. Now no matter where the images come from (admin or user) the images are guaranteed to be a certain size with a certain quality. – Mazhar Zandsalimi Dec 29 '19 at 09:52
0

The easiest solution:

def compress(image):
    im = Image.open(image)
    # create a BytesIO object
    im_io = BytesIO() 
    # save image to BytesIO object
    im.save(im_io, 'JPEG', quality=70) 
    # create a django-friendly Files object
    new_image = File(im_io, name=image.name)
    return new_image
Jota
  • 697
  • 10
  • 24