18

How can I easily resize an image after it has been uploaded in Django? I am using Django 1.0.2 and I've installed PIL.

I was thinking about overriding the save() method of the Model to resize it, but I don't really know how to start out and override it.

Can someone point me in the right direction? Thanks :-)

@Guðmundur H: This won't work because the django-stdimage package does not work on Windows :-(

7 Answers7

26

I recommend using StdImageField from django-stdimage, it should handle all the dirty work for you. It's easy to use, you just specify the dimensions of the resized image in the field definition:

class MyModel(models.Model):
    image = StdImageField(upload_to='path/to/img',  size=(640, 480))

Check out the docs — it can do thumbnails also.

AJJ
  • 7,365
  • 7
  • 31
  • 34
Guðmundur H
  • 11,478
  • 3
  • 24
  • 22
  • This doesn't seem to work on a Windows machine :-( It is a known bug in the django-stdimage package. There is some kind of recursion error –  Mar 08 '09 at 16:26
  • 11
    Windows can't recurse. It's a linux only feature :) – codygman Feb 15 '10 at 08:18
  • Fails on osx 10.7, times out without throwing an error, or giving any meaningful debug information. Permissions are definitely correct / it was working before with the normal file field. – danbgray Mar 10 '12 at 20:44
  • this case does not resize maintaining aspect ratio – Sashko Lykhenko Dec 29 '14 at 14:34
13

You should use a method to handle the uploaded file, as demonstrated in the Django documentation.

In this method, you could concatenate the chunks in a variable (rather than writing them to disk directly), create a PIL Image from that variable, resize the image and save it to disk.

In PIL, you should look at Image.fromstring and Image.resize.

Can Berk Güder
  • 109,922
  • 25
  • 130
  • 137
11

I use this code to handle uploaded images, resize them on memory(whithout saving them permanently on disk), and then saving the thumb on a Django ImageField. Hope can help.

    def handle_uploaded_image(i):
        import StringIO
        from PIL import Image, ImageOps
        import os
        from django.core.files import File
        # read image from InMemoryUploadedFile
        image_str = “”
        for c in i.chunks():
            image_str += c

        # create PIL Image instance
        imagefile  = StringIO.StringIO(image_str)
        image = Image.open(imagefile)

        # if not RGB, convert
        if image.mode not in (“L”, “RGB”):
            image = image.convert(“RGB”)

        #define file output dimensions (ex 60x60)
        x = 130
        y = 130

        #get orginal image ratio
        img_ratio = float(image.size[0]) / image.size[1]

        # resize but constrain proportions?
        if x==0.0:
            x = y * img_ratio
        elif y==0.0:
            y = x / img_ratio

        # output file ratio
        resize_ratio = float(x) / y
        x = int(x); y = int(y)

        # get output with and height to do the first crop
        if(img_ratio > resize_ratio):
            output_width = x * image.size[1] / y
            output_height = image.size[1]
            originX = image.size[0] / 2 - output_width / 2
            originY = 0
        else:
            output_width = image.size[0]
            output_height = y * image.size[0] / x
            originX = 0
            originY = image.size[1] / 2 - output_height / 2

        #crop
        cropBox = (originX, originY, originX + output_width, originY + output_height)
        image = image.crop(cropBox)

        # resize (doing a thumb)
        image.thumbnail([x, y], Image.ANTIALIAS)

        # re-initialize imageFile and set a hash (unique filename)
        imagefile = StringIO.StringIO()
        filename = hashlib.md5(imagefile.getvalue()).hexdigest()+’.jpg’

        #save to disk
        imagefile = open(os.path.join(‘/tmp’,filename), ‘w’)
        image.save(imagefile,’JPEG’, quality=90)
        imagefile = open(os.path.join(‘/tmp’,filename), ‘r’)
        content = File(imagefile)

        return (filename, content)

#views.py

    form = YourModelForm(request.POST, request.FILES, instance=profile)
        if form.is_valid():
            ob = form.save(commit=False)
            try:
                t = handle_uploaded_image(request.FILES[‘icon’])
                ob.image.save(t[0],t[1])
            except KeyError:
                ob.save()
Stefan Manastirliu
  • 3,704
  • 3
  • 24
  • 18
  • UPDATE: < filename = hashlib.md5(i.name.encode("utf8")).hexdigest()+'.jpg' > This will accept utf8 images filename – Stefan Manastirliu Jul 21 '11 at 12:32
  • 2
    You method works fine. Thanks for the code! I would suggest, though, that you change the variable name above from str to something else, because it shadows python BIF function str(). If someone changes a bit the code you have written and uses the BIF bellow the variable declaration, it would cause an error (python would say that str is not callable) – rvnovaes Feb 12 '14 at 00:52
  • Hi, i have a question : here, you upload image on tmp directory : imagefile = open(os.path.join(‘/tmp’,filename), ‘w’) and again , here ob.image.save(t[0],t[1]) ... so what happen next with the image on the tmp directory ??? – laurent Jul 10 '15 at 14:23
7

I highly recommend the sorl-thumbnail app for handling image resizing easily and transparently. It goes in every single Django project I start.

Carl Meyer
  • 122,012
  • 20
  • 106
  • 116
  • The [sorl-thumbnail](https://github.com/sorl/sorl-thumbnail) project has moved to github btw. – Raj Dec 04 '11 at 23:59
  • Sorl has a new mantainer and is preparing with a new release soon, give it a look http://github.com/mariocesar/sorl-thumbnail – Mario César Jan 11 '14 at 01:17
3

Here is a complete solution for ya using a form. I used admin views for this:

class MyInventoryItemForm(forms.ModelForm):

    class Meta:
        model = InventoryItem
        exclude = ['thumbnail', 'price', 'active']

    def clean_photo(self):
        import StringIO
        image_field = self.cleaned_data['photo']
        photo_new = StringIO.StringIO(image_field.read())

        try:
            from PIL import Image, ImageOps

        except ImportError:
            import Image
            import ImageOps

        image = Image.open(photo_new)

        # ImageOps compatible mode
        if image.mode not in ("L", "RGB"):
            image = image.convert("RGB")

        image.thumbnail((200, 200), Image.ANTIALIAS)

        image_file = StringIO.StringIO()
        image.save(image_file, 'png')

        image_field.file = image_file

        return image_field

My inventory model looks like this:

class InventoryItem(models.Model):

    class Meta:
        ordering = ['name']
        verbose_name_plural = "Items"

    def get_absolute_url(self):
        return "/products/{0}/".format(self.slug)

    def get_file_path(instance, filename):

        if InventoryItem.objects.filter(pk=instance.pk):
            cur_inventory = InventoryItem.objects.get(pk=instance.pk)
            if cur_inventory.photo:
                old_filename = str(cur_inventory.photo)
                os.remove(os.path.join(MEDIA_ROOT, old_filename))

        ext = filename.split('.')[-1]
        filename = "{0}.{1}".format(uuid.uuid4(), ext)
        return os.path.join('inventory', filename)
        #return os.path.join(filename)

    def admin_image(self):
        return '<img height="50px" src="{0}/{1}"/>'.format(MEDIA_URL, self.photo)
    admin_image.allow_tags = True

    photo = models.ImageField(_('Image'), upload_to=get_file_path, storage=fs, blank=False, null=False)
    thumbnail = models.ImageField(_('Thumbnail'), upload_to="thumbnails/", storage=fs,     blank=True, null=True)

....

I ended overwriting the save function of the model instead to save the photo and a thumb instead of just resizing the photo:

def save(self):

    # Save this photo instance first
    super(InventoryItem, self).save()

    from PIL import Image
    from cStringIO import StringIO
    from django.core.files.uploadedfile import SimpleUploadedFile

    # Set our max thumbnail size in a tuple (max width, max height)
    THUMBNAIL_SIZE = (200, 200)

    # Open original photo which we want to thumbnail using PIL's Image object
    image = Image.open(os.path.join(MEDIA_ROOT, self.photo.name))

    if image.mode not in ('L', 'RGB'):
        image = image.convert('RGB')

    image.thumbnail(THUMBNAIL_SIZE, Image.ANTIALIAS)

    # Save the thumbnail
    temp_handle = StringIO()
    image.save(temp_handle, 'png')  # image stored to stringIO

    temp_handle.seek(0)  # sets position of file to 0

     # Save to the thumbnail field
     suf = SimpleUploadedFile(os.path.split(self.photo.name)[-1],
        temp_handle.read(), content_type='image/png')  # reads in the file to save it

    self.thumbnail.save(suf.name+'.png', suf, save=False)

    #Save this photo instance again to save the thumbnail
    super(InventoryItem, self).save()

Both work great though depending on what you want to do :)

radtek
  • 34,210
  • 11
  • 144
  • 111
2

I know this is old, but for anybody stumbling upon it, there is a package, django-thumbs at Django-thumbs - Easy powerful thumbnails for Django integrated with StorageBackend, which automatically generates thumbnails in sizes you specify, or none if you don't. You then call the thumbnail you want with the dimensions you want.

For instance, if you want an image to have thumbnails of 64x64 and 128x128, you simply import thumbs.models.ImageWithThumbsField, and use it in place of ImageField. Add a parameter sizes=((64,64),(128,128)) to the field definition, then from your template you can call:

{{ ClassName.field_name.url_64x64 }}

and

{{ ClassName.field_name.url_128x128 }}

to display the thumbnails. Voila! All the work is done in this package for you.

Furbeenator
  • 8,106
  • 4
  • 46
  • 54
  • If you use South for database maintenance, you will also need to add an introspection, as such: – Furbeenator Apr 12 '13 at 20:59
  • `from south.modelsinspector import add_introspection_rules add_introspection_rules([ ( [models.ImageField], # Class(es) these apply to [], # Positional arguments (not used) { # Keyword argument "sizes": ["sizes", {"default": None}], }, ), ], ["^django_thumbs\.db\.models\.ImageWithThumbsField"])` – Furbeenator Apr 12 '13 at 21:00
0

If you are using Django Rest Framework, this might of use:

First define function to compress and resize image

def compress_image(photo):
# start compressing image
image_temporary = Image.open(photo)
output_io_stream = BytesIO()
image_temporary.thumbnail((1250, 1250), Image.ANTIALIAS)

# change orientation if necessary
for orientation in ExifTags.TAGS.keys():
    if ExifTags.TAGS[orientation] == 'Orientation':
        break
exif = dict(image_temporary._getexif().items())
# noinspection PyUnboundLocalVariable
if exif.get(orientation) == 3:
    image_temporary = image_temporary.rotate(180, expand=True)
elif exif.get(orientation) == 6:
    image_temporary = image_temporary.rotate(270, expand=True)
elif exif.get(orientation) == 8:
    image_temporary = image_temporary.rotate(90, expand=True)

# saving output
image_temporary.save(output_io_stream, format='JPEG', quality=75, optimize=True, progressive=True)
output_io_stream.seek(0)
photo = InMemoryUploadedFile(output_io_stream, 'ImageField', "%s.jpg" % photo.name.split('.')[0],
                             'image/jpeg', getsizeof(output_io_stream), None)
return photo

Second, now you can use the function in Serializers:

class SomeSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
    # сжимаем рисунок
    if 'photo' in validated_data:           
        validated_data.update({'photo': compress_image(validated_data['photo'])})

    return super(SomeSerializer, self).update(instance, validated_data)

def create(self, validated_data):
    # сжимаем рисунок
    if 'photo' in validated_data:
        validated_data.update({'photo': compress_image(validated_data['photo'])})

    return super(SomeSerializer, self).create(validated_data)
  • Does this answer is really to solve the problem of the question or you you are guessing a solution after approx. 10 years later since when the question was asked? – Atul Dwivedi Feb 28 '19 at 19:40