Django / PIL - save thumbnail version right when image is uploaded

19,565

Solution 1

Based on xjtian's answer. This works for Python 3:

import os.path
from PIL import Image
from io import BytesIO
from django.core.files.base import ContentFile
from .my_app_settings import THUMB_SIZE    

class Photo(models.Model):
    photo = models.ImageField(upload_to='photos')
    thumbnail = models.ImageField(upload_to='thumbs', editable=False)

    def save(self, *args, **kwargs):

        if not self.make_thumbnail():
            # set to a default thumbnail
            raise Exception('Could not create thumbnail - is the file type valid?')

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

    def make_thumbnail(self):

        image = Image.open(self.photo)
        image.thumbnail(THUMB_SIZE, Image.ANTIALIAS)

        thumb_name, thumb_extension = os.path.splitext(self.photo.name)
        thumb_extension = thumb_extension.lower()

        thumb_filename = thumb_name + '_thumb' + thumb_extension

        if thumb_extension in ['.jpg', '.jpeg']:
            FTYPE = 'JPEG'
        elif thumb_extension == '.gif':
            FTYPE = 'GIF'
        elif thumb_extension == '.png':
            FTYPE = 'PNG'
        else:
            return False    # Unrecognized file type

        # Save thumbnail to in-memory file as StringIO
        temp_thumb = BytesIO()
        image.save(temp_thumb, FTYPE)
        temp_thumb.seek(0)

        # set save=False, otherwise it will run in an infinite loop
        self.thumbnail.save(thumb_filename, ContentFile(temp_thumb.read()), save=False)
        temp_thumb.close()

        return True

Solution 2

To do this, you should add a new ImageField to your current UserImages model to hold the thumbnail, then override your the save method to create and save the thumbnail after the full image is saved.

I've adapted the following snippet of code from one of my projects that did exactly this, I'm pretty sure this will do exactly what you need it to do:

from cStringIO import StringIO
import os

from django.db import models
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage as storage

from PIL import Image

# Thumbnail size tuple defined in an app-specific settings module - e.g. (400, 400)
from app.settings import THUMB_SIZE

class Photo(models.Model):
    """
    Photo model with automatically generated thumbnail.
    """
    photo = models.ImageField(upload_to='photos')
    thumbnail = models.ImageField(upload_to='thumbs', editable=False)

    def save(self, *args, **kwargs):
        """
        Make and save the thumbnail for the photo here.
        """
        super(Photo, self).save(*args, **kwargs)
        if not self.make_thumbnail():
            raise Exception('Could not create thumbnail - is the file type valid?')

    def make_thumbnail(self):
        """
        Create and save the thumbnail for the photo (simple resize with PIL).
        """
        fh = storage.open(self.photo.name, 'r')
        try:
            image = Image.open(fh)
        except:
            return False

        image.thumbnail(THUMB_SIZE, Image.ANTIALIAS)
        fh.close()

        # Path to save to, name, and extension
        thumb_name, thumb_extension = os.path.splitext(self.photo.name)
        thumb_extension = thumb_extension.lower()

        thumb_filename = thumb_name + '_thumb' + thumb_extension

        if thumb_extension in ['.jpg', '.jpeg']:
            FTYPE = 'JPEG'
        elif thumb_extension == '.gif':
            FTYPE = 'GIF'
        elif thumb_extension == '.png':
            FTYPE = 'PNG'
        else:
            return False    # Unrecognized file type

        # Save thumbnail to in-memory file as StringIO
        temp_thumb = StringIO()
        image.save(temp_thumb, FTYPE)
        temp_thumb.seek(0)

        # Load a ContentFile into the thumbnail field so it gets saved
        self.thumbnail.save(thumb_filename, ContentFile(temp_thumb.read()), save=True)
        temp_thumb.close()

        return True

Solution 3

I wrote it based by ziiiro's answer. It works fine with Django2.2.1. Need process for save for path of image field.

models.py

from django.db import models
from my.images import make_thumbnail


class Image(models.Model):
    image = models.ImageField(upload_to='')
    thumbnail = models.ImageField(upload_to='', editable=False)
    icon = models.ImageField(upload_to='', editable=False)

    def save(self, *args, **kwargs):
        # save for image
        super(Image, self).save(*args, **kwargs)

        make_thumbnail(self.thumbnail, self.image, (200, 200), 'thumb')
        make_thumbnail(self.icon, self.image, (100, 100), 'icon')

        # save for thumbnail and icon
        super(Image, self).save(*args, **kwargs)

my.images.py

from django.core.files.base import ContentFile
import os.path
from PIL import Image
from io import BytesIO


def make_thumbnail(dst_image_field, src_image_field, size, name_suffix, sep='_'):
    """
    make thumbnail image and field from source image field

    @example
        thumbnail(self.thumbnail, self.image, (200, 200), 'thumb')
    """
    # create thumbnail image
    image = Image.open(src_image_field)
    image.thumbnail(size, Image.ANTIALIAS)

    # build file name for dst
    dst_path, dst_ext = os.path.splitext(src_image_field.name)
    dst_ext = dst_ext.lower()
    dst_fname = dst_path + sep + name_suffix + dst_ext

    # check extension
    if dst_ext in ['.jpg', '.jpeg']:
        filetype = 'JPEG'
    elif dst_ext == '.gif':
        filetype = 'GIF'
    elif dst_ext == '.png':
        filetype = 'PNG'
    else:
        raise RuntimeError('unrecognized file type of "%s"' % dst_ext)

    # Save thumbnail to in-memory file as StringIO
    dst_bytes = BytesIO()
    image.save(dst_bytes, filetype)
    dst_bytes.seek(0)

    # set save=False, otherwise it will run in an infinite loop
    dst_image_field.save(dst_fname, ContentFile(dst_bytes.read()), save=False)
    dst_bytes.close()
Share:
19,565

Related videos on Youtube

SilentDev
Author by

SilentDev

Updated on June 05, 2022

Comments

  • SilentDev
    SilentDev almost 2 years

    This is my forms.py:

    class UploadImageForm(forms.ModelForm):
        class Meta:
            model = UserImages
            fields = ['photo']
    

    and this is my models.py:

    class UserImages(models.Model):
        user = models.ForeignKey(User)
        photo = models.ImageField(upload_to=get_file_path)
    

    and this is my view:

    def uploadImageView(request):
        if request.method == 'POST':
            form = UploadImageForm(request.POST, request.FILES)
            if form.is_valid():
                instance = form.save(commit=False)
                instance.user = request.user
                instance.save()
                return redirect('/')
        else:
            form = UploadImageForm()
    
        return render(request, 'uploadImagePage.html', {'uploadImageForm': form})
    

    But this only saves the image being uploaded. How do I save a thumbnail version of the image as well with the thumbnail version of the image having the exact same name except with the word 'thumbail' after it?

    The tutorials I read said I can do

    im = Image.open(infile)
    im.thumbnail(size, Image.ANTIALIAS)
    

    to get a thumbnail but in my situation, the image isn't even saved yet.

  • SilentDev
    SilentDev almost 10 years
    I'll use this as a backup but I am looking for a way to implement a solution it from scratch. I don't think it should be hard, I just can't find the right PIL / Pillow documentation.
  • SilentDev
    SilentDev almost 10 years
    right but the thing is, what is im? If you look at my code, I don't specify im. I do instance.save (and instance = form.save). So I don't have an im to begin with since I cannot open the file (in the example you liked to, they first did im = Image.open(infile)). How do I open a file which I do not know the location of yet since I did not save it yet? I want to save an image and a thumbnail version of the image at the same time.
  • Albert Tugushev
    Albert Tugushev almost 10 years
    You should make the tumbnail after save a model instance. Save an instance like this: obj = instance.save(). After that you can get an image instance like this: im = Image.open(obj.photo.path). But it's better to override a save method of a class UploadImageForm and deal with tumbnail.
  • stupidbodo
    stupidbodo over 7 years
    Wouldn't it be better to throw exceptions within make_thumbnail instead of returning False? This way, you can know the exact cause of the exceptions instead of having a generic exception message
  • user2012801
    user2012801 about 7 years
    This throws ValueError: seek of closed file when trying to edit a photo instance in the admin app without reuploading the image (django 1.11, python 3.6), which is kind of handy as you don't need to regen the thumbnail if the image hasn't changed. To take this into account can add if not self.photo.closed: before calling self.make_thumbnail()
  • user3750325
    user3750325 over 4 years
    Will the make_thumbnail method be called on edit if a non-image field of this record is updated?
  • Andy Stagg
    Andy Stagg over 4 years
    This works until you move off the base media root, IE, upload_to='my_sub_folder'. I was ending up with the thumbnail created in mediaroot/my_sub_folder/my_subfolder/. Updating the dst_name, dst_ext line to: dst_name, dst_ext = os.path.splitext(os.path.basename(src_image_field.name)) and the dst_fname line to dst_fname = '{}{}{}{}'.format(dst_name, sep, name_suffix, dst_ext) corrected the issue for me.
  • MrColes
    MrColes over 4 years
    does this mean the thumbnails are generated every time save is called, even if photo is not changed (e.g., if the model had extra fields and you wanted to update those)?
  • coredumped0x
    coredumped0x almost 3 years
    I built on your response and it really helped me. going from one bug to another, I questioned my sanity. I even thought of getting a gallon of gas and burning my computer.