Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django: changing image size and upload to S3

I have inherited a Django Project and we have moved images to S3

One of the models is a typical user profile

class Profile(UUIDBase):

    first_name = models.CharField(_("First Name"), max_length=20)
    last_name = models.CharField(_("Last Name"), max_length=20, null=True)
    profile_image = models.ImageField(
        _("Profile Image"),
        upload_to=profile_image_name,
         max_length=254,
        blank=True,
        null=True
    )
    profile_image_thumb = models.ImageField(
        _("Profile Image Thumbnail"),
        upload_to=profile_image_name,
        max_length=254,
        blank=True,
       null=True
    )
    ... other fields

Where profile_image_name is a function:

def profile_image_name(instance, filename):
    if filename:
        target_dir = 'uploads/profile_img/'
        _, ext = filename.rsplit('.', 1)
        filename = str(instance.uid) + '.' + ext
        return '/'.join([target_dir, filename])

I have a bit of code that worked:

@shared_task
def resize_image(image_path, dim_x, append_str='_resized', **kwargs):
    '''
    resize any image_obj while maintaining aspect ratio
    '''
    orig = storage.open(image_path, 'r')
    im = Image.open(orig, mode='r')
    new_y = (float(dim_x) * float(im.height)) / float(im.width)
    new_im = im.resize((dim_x, int(new_y)), Image.ANTIALIAS)
    img_path, img_name = path.split(image_path)
    file_name, img_ext = img_name.rsplit('.', 1)
    new_img_path = path.join(img_path, file_name + append_str + '.' + img_ext)
    try:
        new_f = storage.open(new_img_path, 'w')
    except IOError as e:
        logger.critical("Caught IOError in {}, {}".format(__file__, e))
        ravenclient.captureException()
        return None
    try:
        new_im.save(new_f)
    except IOError as e:
        logger.critical("Caught IOError in {}, {}".format(__file__, e))
        ravenclient.captureException()
       return None
    except Exception as e:
       logger.critical("Caught unhandled exception in {}. {}".format(
        __file__, e)
       )
       ravenclient.captureException()
       return None
    im.close()
    new_im.close()
    new_f.close()
    return new_img_path

Which is called from a post_save signal handler :

@receiver(post_save, sender=Profile, dispatch_uid='resize_profile_image')
def resize_profile_image(sender, instance=None, created=False, **kwargs):
    if created:
        if instance.profile_image:
            width, height = image_dimensions(instance.profile_image.name)
            print(width, height)
            if width > MAX_WIDTH:
                result = resize_image.delay(instance.profile_image.name, MAX_WIDTH)
                instance.profile_image.name = result.get()
            if width > THUMB_WIDTH:
                result = resize_image.delay(
                    instance.profile_image.name,
                    THUMB_WIDTH, 
                    append_str='_thumb'
                )
                instance.profile_image_thumb.name = result.get()
            try:
                instance.save()
            except Exception as e:
                log.critical("Unhandled exception in {}, {}".format(__name__, e))
                ravenclient.captureException()

The intent is to take uploaded images and resize them 1) to the max width that a mobile device can display and 2) to a 50 pixel thumbnail for use in the mobile app.

When I look on S3, I do not see my resized images or thumbnails. Yet the unit tests (which are thorough) don't give any errors.

When I get the image dimensions:

def image_dimensions(image_path):
    f = storage.open(image_path, 'r')
    im = Image.open(f, 'r')
    height = im.height
    width = im.width
    im.close()
    f.close()
    return (width, height)

There is no problem accessing the object's ImageField. I get no error when I use default_storage to open the instance's profile_image. The PIL method

new_im = im.resize((dim_x, int(new_y)), Image.ANTIALIAS) does return a new instance of class 'PIL.Image.Image'.

In fact (pardon my verbosity)

This does not raise an error:

>>> u = User(email="[email protected]", password="sdfbskjfskjfskjdf")
>>> u.save()
>>> p = Profile(user=u, profile_image=create_image_file())
>>> p.save()
>>> from django.core.files.storage import default_storage as storage
>>> orig = storage.open(p.profile_image.name, 'r')
>>> orig
<S3BotoStorageFile: uploads/profile_img/b0fd4f00-cce6-4dd3-b514-4c46a801ab19.jpg>
>>> im = Image.open(orig, mode='r')
>>> im
<PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=5000x5000 at 0x10B8F1FD0>
>>> im.__class__
<class 'PIL.JpegImagePlugin.JpegImageFile'>
>>> dim_x = 500
>>> new_y = (float(dim_x) * float(im.height)) / float(im.width)
>>> new_im = im.resize((dim_x, int(new_y)), Image.ANTIALIAS)
>>> new_im.__class__
<class 'PIL.Image.Image'>
>>> img_path, img_name = path.split(p.profile_image.name)
>>> file_name, img_ext = img_name.rsplit('.', 1)
>>> append_str='_resized'
>>> new_img_path = path.join(img_path, file_name + append_str + '.' + img_ext)
>>> new_f = storage.open(new_img_path, 'w')
>>> new_f
<S3BotoStorageFile: uploads/profile_img/b0fd4f00-cce6-4dd3-b514-4c46a801ab19_resized.jpg>
>>> new_im.save(new_f)  #### This does NOT create an S3 file!!!!
>>> im.close()
>>> new_im.close()
>>> new_f.close()

>>> p.save() uploads the new profile image to S3. I was expecting >>> new_im.save(new_f) to write the Image file to S3. But it does not.

Any insight or help is greatly appreciated and thank you for taking the time to look at this problem.

Edit ...

My settings:

AWS_STORAGE_BUCKET_NAME = 'testthis'
AWS_S3_CUSTOM_DOMAIN = '%s.s3.amazonaws.com' % AWS_STORAGE_BUCKET_NAME
MEDIAFILES_LOCATION = 'media'
MEDIA_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, MEDIAFILES_LOCATION)
DEFAULT_FILE_STORAGE = 'custom_storages.MediaStorage'

Where custom_storage.py is

from django.conf import settings
from storages.backends.s3boto import S3BotoStorage

class MediaStorage(S3BotoStorage):
    location = settings.MEDIAFILES_LOCATION
    bucket_name = settings.AWS_STORAGE_BUCKET_NAME
like image 727
fiacre Avatar asked Aug 02 '16 21:08

fiacre


Video Answer


1 Answers

I think the whole setup is crazy. I'd strongly suggest you look into using a library like django-versatileimagefield. The implementation would look like this:

from versatileimagefield.fields import VersatileImageField
from versatileimagefield.image_warmer import VersatileImageFieldWarmer

class Profile(UUIDBase):
    first_name = models.CharField(_("First Name"), max_length=20)
    last_name = models.CharField(_("Last Name"), max_length=20, null=True)
    image = VersatileImageFied(upload_to='uploads/profile_img/', blank=True, null=True)


@receiver(models.signals.post_save, sender=Profile)
def warm_profile_image(sender, instance, **kwargs):
    if instance.image:
        VersatileImageFieldWarmer(instance_or_queryset=instance, rendition_key_set='profile_image', image_attr='image', verbose=True).warm()

And in your settings:

VERSATILEIMAGEFIELD_RENDITION_KEY_SETS = {
    'profile_image': [
        ('cropped', 'crop__400x400'),
        ('thumbnail', 'thumbnail__20x20')
    ]
}

The Profile warmer creates representations saved to S3 no problem. You can access either the full image as profile.image or the different versions as profile.image.cropped and profile.image.thumbnail. The library even allows you to set up a point of interest so the cropping happens around a specific center point in the image.

Serializers if using DRF:

from versatileimagefield.serializers import VersatileImageFieldSerializer   

class ProfileSerializer(serializers.ModelSerializer):
    image = VersatileImageFieldSerializer(
        sizes=[
            ('cropped', 'crop__400x400'),
            ('thumbnail', 'thumbnail__20x20')
        ],
        required=False
    )
    ... other fields and the Meta class
like image 73
Jura Brazdil Avatar answered Sep 30 '22 06:09

Jura Brazdil