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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With