Django: changing image size and upload to S3 - python

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="root#groupon.com", 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

The issue seems to be related to PIL's JPEG library:
>>> u = User(email="root#groupon.com", 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.png>
>>> im = Image.open(orig, mode='r')
>>> im.__class__
<class 'PIL.PngImagePlugin.PngImageFile'>
>>> 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.png>
>>> new_im.save(new_f) #### This does create a file on S3!
>>> im.close()
>>> new_im.close()
>>> new_f.close()

The problem is in django-storages. S3BotoStorageFile's _file attribute is a SpooledTemporaryFile which works for png files, but not jpg files.
See: https://github.com/jschneier/django-storages/issues/155

Related

How to work with uploaded file using Django

I have Optical Character Recognition (OCR) project. I am generating an API using Django Framework. The API should look like as below:
{
"id": 1,
"title": "PDF Title",
"input": "input.pdf",
"output": "output.pdf"
}
My models.py file as below:
from django.db import models
from .create_pdf_output import *
# Create your models here.
class Document(models.Model):
title = models.CharField(max_length=255)
pdf_input = models.FileField(upload_to='documents/inputs', max_length=200, blank=False)
pdf_output = models.FileField(upload_to='documents/outputs', max_length=200, blank=True)
def save(self):
self.pdf_output = create_pdf_output(self.pdf_input)
super(Document, self).save()
def __str__(self):
return self.title
In models.py file I call create_pdf_file.py which should perform OCR operations on pdf input
create_pdf_output.py
import shutil
import cv2
import numpy as np
import img2pdf
from pdf2image import convert_from_path
import os
def create_pdf_output(pdf_input):
pdf = str(pdf_input)
pdf_name = os.path.splitext(pdf)[0] + "_out.pdf"
pages = convert_from_path(pdf_input, 500)
if not os.path.exists('images'):
os.mkdir('images')
for ind, page in enumerate(pages):
page.save(f'images/out{ind}.jpg', 'JPEG')
for ind, img in enumerate(os.listdir('images')):
img_rgb = cv2.imread(os.path.join('images', img))
img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)
template = cv2.imread('templates.jpg', 0)
w, h = template.shape[::-1]
res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
threshold = 0.8
loc = np.where(res >= threshold)
for pt in zip(*loc[::-1]):
cropped_image = img_rgb[pt[1]:pt[1]+h, pt[0]:pt[0]+ 4*h]
blurred = cv2.blur(cropped_image, (50,50))
img_rgb[pt[1]:pt[1]+h, pt[0]:pt[0]+ 4*h] = blurred
if not os.path.exists('results'):
os.mkdir('results')
cv2.imwrite(f'results/res{ind}.jpg', img_rgb)
dirname = 'images'
with open(pdf_name, 'wb') as f:
imgs = []
for fname in os.listdir(dirname):
if not fname.endswith('.jpg'):
continue
path = os.path.join(dirname, fname)
if os.path.isdir(path):
continue
imgs.append(path)
f.write(img2pdf.convert(imgs))
shutil.rmtree('images')
shutil.rmtree('results')
return pdf_name
However, since create_input_output function takes FieldFile as input it generates an error as below.
expected str, bytes or os.PathLike object, not FieldFile
My question is how can I work with input file, so I can generate output pdf file?
views.py
# pdf api
class OcrPDfDataApiView(APIView):
parser_class = [MultiPartParser,]
def post(self, request, format=None):
if 'data' not in request.data:
raise ParseError("Empty content")
filename = "whatever.pdf" # received file name
file_obj = request.data['data']
with default_storage.open('tmp/'+filename, 'wb+') as destination:
for chunk in file_obj.chunks():
destination.write(chunk)
dirName = os.path.dirname(__file__)
cwd = Path.cwd()
filename = os.path.join(cwd, 'media','tmp','whatever.pdf')
texts = print_pages(filename)
return Response(texts)
utils.py
import pytesseract
from PIL import Image
from PIL import ImageFilter
import requests
import docx2txt
import pdf2image
import PyPDF2
import pyttsx3
import os
import io
def print_pages(pdf_file):
images = pdf2image.convert_from_path(pdf_file)
for pages,img in enumerate(images):
text = pytesseract.image_to_string(img)
print(text)
return text

how to override django files?

I have a function that crops user images but I don't want the model to have 2 fields so I made a function that overrides the original file and I noticed that the function works well on normal files but when I add the function to the view new file is made but at the media directory not even the the specified folder so how can i override files by Django ?
models.py
# defining directory for every patient
def user_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/patient_<id>/<filename>
return 'patient_{0}/{1}'.format(instance.patient.id, filename)
class UploadedImages(models.Model):
patient = models.ForeignKey(Patient,on_delete=models.CASCADE,related_name='images')
pre_analysed = models.ImageField(upload_to = user_directory_path ,
verbose_name = 'Image')
upload_time = models.DateTimeField(default=timezone.now)
the cropping function:
import os
from PIL import Image
def crop(corrd, file ,path,pk):
image = Image.open(file) #To open the image as file.path won't open
path_1 , fn = os.path.split(path) #which is MEDIA_ROOT/and <filename>
patient_dir = 'patient_{}'.format(pk) #to get the user directory
path_ = path_1+patient_dir+fn #MEDIA_ROOT/patient_<id>/<filename>
cropped_image = image.crop(corrd)
resized_image = cropped_image.resize((384, 384), Image.ANTIALIAS)
resized_image.save(path_)
return path_
views.py
if form.is_valid():
image = form.save(commit=False)
x = float(request.POST.get('x'))
y = float(request.POST.get('y'))
w = float(request.POST.get('width'))
h = float(request.POST.get('height'))
print(x)
print(y)
print(w)
print(h)
crop((x,y,w+x,y+h),image.pre_analysed,image.pre_analysed.path)
image.patient = patient
messages.success(request,"Image added successfully!")
image.save()
forms.py
class ImageForm(ModelForm):
x = forms.FloatField(widget=forms.HiddenInput())
y = forms.FloatField(widget=forms.HiddenInput())
width = forms.FloatField(widget=forms.HiddenInput())
height = forms.FloatField(widget=forms.HiddenInput())
class Meta:
model = UploadedImages
fields = ('pre_analysed', 'x', 'y', 'width', 'height', )
so what do I need to do here?
thanks in advance.
The path you get from .path is relative to your MEDIA_ROOT folder, which may prevent your crop function from being able to open() the image file.
You can then make a new cropped file somewhere in your MEDIA_ROOT. Be sure to use os.makedirs() to create all the directories between here and the file. Also I don't see you saving the path returned from crop() back into the ImageField.

Testing an ImageField in DRF

I am using Django Imagekit in models.py:
from imagekit.models import ProcessedImageField
class AltroUser(models.Model):
first_name = models.CharField(_('first name'), max_length=30)
image = ProcessedImageField(upload_to='media/path',
default='user_default.jpg',
processors=[ResizeToFill(640, 640)],
format='JPEG',
options={'quality': 60})
serializers.py:
class UserRegistrationSerializer(Serializer):
first_name = serializers.CharField()
image = serializers.ImageField()
I am trying to test the image field. I have tried following methods:
def get_test_image():
try:
image = DjangoFile(open(os.path.join(django_settings.MEDIA_ROOT, 'user_default.jpg'),
mode='rb'))
return image
except (OSError, IOError) as e:
return None
def get_test_image1():
file = io.BytesIO()
image = Image.new('RGBA', size=(100, 100), color=(155, 0, 0))
image.save(file, 'png')
file.name = 'test.png'
file.seek(0)
return SimpleUploadedFile('abc.jpg', file.read())
def get_test_image2():
path = os.path.join(django_settings.MEDIA_ROOT, 'user_default.jpg')
file = File(open(path, 'r+b'))
return SimpleUploadedFile('abc.jpg', file.read())
I have tried calling the above three methods to set the value of the image key but none of them worked.
For get_test_image(), I get a response "The submitted file is empty."
For get_test_image1() and get_test_image2(), I get a response "The submitted file is empty." with an exception before in the data image field '_io.BytesIO' object has no attribute 'encoding'.
I don't understand what am I missing. Please help.
Here's an example of what I'm using to generate test image:
from StringIO import StringIO
from PIL import Image
from django.core.files import File
def get_image_file(name='test.png', ext='png', size=(50, 50), color=(256, 0, 0)):
file_obj = StringIO()
image = Image.new("RGBA", size=size, color=color)
image.save(file_obj, ext)
file_obj.seek(0)
return File(file_obj, name=name)
In Pillow==8.3.1, using StringIO() gave me this error:
TypeError: string argument expected, got 'bytes'.
I used BytesIO() instead and it worked like a charm
from io import BytesIO
def get_image_file(
self, name="test.png", ext="png", size=(50, 50), color=(256, 0, 0)
):
file_obj = BytesIO()
image = Image.new("RGBA", size=size, color=color)
image.save(file_obj, ext)
file_obj.seek(0)
return File(file_obj, name=name)

PIL to Django ImageField

I try to create an image from an url and save it in my django model. If the first part works fine, I do not know how to associate the generated file to my object.
This is my function to generate the image file:
def get_remote_image(image_url, merchant_product_path):
im = None
name = ''
r = requests.get(image_url, stream=True)
if r.status_code == 200:
name = urlparse(image_url).path.split('/')[-1]
full_path = os.path.join(settings.MEDIA_ROOT, merchant_product_path)
if not os.path.exists(full_path):
os.makedirs(full_path)
im = Image.open(r.raw)
if im.mode != "RGB":
im = im.convert("RGB")
im.thumbnail((500, 500), Image.ANTIALIAS)
im.save(full_path + name, 'JPEG')
return {'im': im, 'name': name}
And now, the part to associate this file to my object:
i = get_remote_image(row['pict'], m.get_products_media_path())
obj, created = ProductLine.objects.update_or_create(
...
...
...
)
if i['im'] is not None:
try:
obj.main_picture.save(
i['name'],
ContentFile(i['im']),
save=True)
except TypeError:
continue
This code works but unfortunately, mu pictures are created in the correct folder, objects are created/update but each one has no picture file :(
Can someone tell me what's wrong ?
I've finally found a solution:
def get_remote_image(image_url):
im = None
name = ''
r = requests.get(image_url, stream=True)
if r.status_code == 200:
name = urlparse(image_url).path.split('/')[-1]
i = Image.open(r.raw)
buffer = BytesIO()
if i.mode != "RGB":
i = i.convert("RGB")
i.thumbnail((500, 500), Image.ANTIALIAS)
i.save(buffer, format='JPEG')
im = InMemoryUploadedFile(
buffer,
None,
name,
'image/jpeg',
buffer.tell(),
None)
return {'im': im, 'name': name}
and then:
obj, created = ProductLine.objects.update_or_create(
...
...
...
)
i = get_remote_image(row['pict'])
obj.main_picture.save(
os.path.join(m.get_products_image_path(), i['name']),
i['im'],
save=True)
Hope this will help some other users in this situation.
With a model like :
class ProductLine(models.Model):
name = models.CharField(max_length=250, null=True)
image = models.ImageField(null=True)
You can directly link the picture on your computer using is path instead of his binary content.
obj, created = ProductLine.objects.update_or_create(...)
obj.image.name = "/path/to/the/file"
obj.save()

Django create thumbnail with PIL when i upload image

I use django-dropbox for the image storage on my website.
When i upload the image i want to automatically create an thumbnail and save it.
I tryed different methods but with no success
Any help will be apreciated
I get this exception error:
cannot identify image file
Here is the code:
class Product(models.Model):
image = models.ImageField(upload_to='images', storage=STORAGE, null=True, blank=True)
thumb = models.ImageField(upload_to='thumbs', storage=STORAGE, null=True, blank=True)
def __unicode__(self):
return "%s,%s" %(self.id,self.title)
def save(self):
# create a thumbnail
self.create_thumbnail()
super(Product, self).save()
def create_thumbnail(self):
# create a thumbnail
#self.create_thumbnail()
from PIL import Image
from cStringIO import StringIO
from django.core.files.uploadedfile import SimpleUploadedFile
import os
try:
DJANGO_TYPE = self.image.file.content_type
print DJANGO_TYPE
if DJANGO_TYPE == 'image/jpeg':
PIL_TYPE = 'jpeg'
FILE_EXTENSION = 'jpg'
elif DJANGO_TYPE == 'image/png':
PIL_TYPE = 'png'
FILE_EXTENSION = 'png'
else:
print error
print "Working1"
print StringIO(self.image.read())
im = Image.open(StringIO(self.image.read()))
size = 128, 128
im.thumbnail(size, Image.ANTIALIAS)
# Save the thumbnail
print "Working2"
temp_handle = StringIO()
im.save(temp_handle, PIL_TYPE)
temp_handle.seek(0)
# Save image to a SimpleUploadedFile which can be saved into
# ImageField
print "Working3"
suf = SimpleUploadedFile(os.path.split(self.image.name)[-1],
temp_handle.read(), content_type=DJANGO_TYPE)
# Save SimpleUploadedFile into image field
self.thumb.save('%s_thumbnail.%s'%(os.path.splitext(suf.name)[0],FILE_EXTENSION), suf, save=True)
print "Working4"
except Exception as e:
print e

Categories